Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface Builder: Typed segues on a per scene basis #364

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/SwiftGenCLI/templates/ib/scenes-swift4.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {{module}}
{% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
{{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
{% endif %}
{% for scene in storyboard.scenes %}
{% for scene in storyboard.scenes where scene.identifier %}

{% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set sceneClass %}{% call className scene %}{% endset %}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftGenCLI/templates/ib/scenes-swift5.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {{module}}
{% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
{{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
{% endif %}
{% for scene in storyboard.scenes %}
{% for scene in storyboard.scenes where scene.identifier %}

{% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set sceneClass %}{% call className scene %}{% endset %}
Expand Down
94 changes: 92 additions & 2 deletions Sources/SwiftGenCLI/templates/ib/segues-swift4.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,100 @@ import {{module}}
// MARK: - Storyboard Segues

// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
{% macro moduleName item %}{% filter removeNewlines %}
{% if item.moduleIsPlaceholder %}
{{ env.PRODUCT_MODULE_NAME|default:param.module }}
{% else %}
{{ item.module }}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro className item %}{% filter removeNewlines %}
{% set module %}{% call moduleName item %}{% endset %}
{% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
{{module}}.
{% endif %}
{{item.type}}
{% endfilter %}{% endmacro %}
{% set enumName %}{{param.enumName|default:"StoryboardSegue"}}{% endset %}
{% set unnamedSegue %}{{param.unnamedSegueCaseName|default:"unnamedSegue"}}{% endset %}
{% for scene in customSceneTypes where scene.segues %}
{% set sceneClass %}{% call className scene %}{% endset %}
{{accessModifier}} extension {{sceneClass}} {
{{accessModifier}} enum {{enumName}}: String {
{% for segue in scene.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
}

{{accessModifier}} func perform(segue: {{enumName}}, sender: Any? = nil) {
let identifier = {% if isAppKit %}NSStoryboardSegue.Identifier({% endif %}segue.rawValue{% if isAppKit %}){% endif %}
performSegue(withIdentifier: identifier, sender: sender)
}

{{accessModifier}} enum Typed{{enumName}} {
{% set hasUnnamedSegue %}{% for segue in scene.segues where not scene.identifier %}1{% endfor %}{% endset %}
{% for segue in scene.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% filter removeNewlines:"leading" %}
{% if segue.customClass or segue.destination %}(
{% if segue.destination %}destination: {% call className segue.destination %}{% endif %}
{% if segue.customClass and segue.destination %}, {% endif %}
{% if segue.customClass %}segue: {% call className segue %}{% endif %}
){% endif %}
{% endfilter %}
{% endfor %}
{% if hasUnnamedSegue %}
case {{unnamedSegue}}
{% endif %}

// swiftlint:disable cyclomatic_complexity
init(segue: {{prefix}}StoryboardSegue) {
switch segue.identifier{% if isAppKit %}?.rawValue{% endif %} ?? "" {
{% for segue in scene.segues where segue.identifier %}
case "{{segue.identifier}}":
{% if segue.customClass %}
{% set segueClass %}{% call className segue %}{% endset %}
guard let segue = segue as? {{segueClass}} else {
fatalError("Segue '{{segue.identifier}}' is not of the expected type {{segueClass}}.")
}
{% endif %}
{% if segue.destination %}
{% set destinationClass %}{% call className segue.destination %}{% endset %}
{% if destinationClass != "UIKit.UIViewController" %}
guard let vc = segue.destination{% if isAppKit %}Controller{% endif %} as? {{destinationClass}} else {
fatalError("Destination of segue '{{segue.identifier}}' is not of the expected type {{destinationClass}}.")
}
{% else %}
let vc = segue.destination{% if isAppKit %}Controller{% endif %}
{% endif %}
{% endif %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
self = .{{segueID|escapeReservedKeywords}}{% filter removeNewlines:"leading" %}
{% if segue.customClass or segue.destination %}(
{% if segue.destination %}destination: vc{% endif %}
{% if segue.customClass and segue.destination %}, {% endif %}
{% if segue.customClass %}segue: segue{% endif %}
){%endif %}
{% endfilter %}
{% endfor %}
{% if hasUnnamedSegue %}
case "":
self = .{{unnamedSegue}}
{% endif %}
default:
fatalError("Unrecognized segue '\(segue.identifier{% if isAppKit %}?.rawValue{% endif %} ?? "")' in {{sceneClass}}")
}
}
// swiftlint:enable cyclomatic_complexity
}
}

{% endfor %}
{{accessModifier}} enum {{enumName}} {
{% for storyboard in storyboards where storyboard.segues %}
{{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
{% for segue in storyboard.segues %}
{% for segue in storyboard.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
Expand Down
93 changes: 91 additions & 2 deletions Sources/SwiftGenCLI/templates/ib/segues-swift5.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,99 @@ import {{module}}
// MARK: - Storyboard Segues

// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
{% macro moduleName item %}{% filter removeNewlines %}
{% if item.moduleIsPlaceholder %}
{{ env.PRODUCT_MODULE_NAME|default:param.module }}
{% else %}
{{ item.module }}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro className item %}{% filter removeNewlines %}
{% set module %}{% call moduleName item %}{% endset %}
{% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
{{module}}.
{% endif %}
{{item.type}}
{% endfilter %}{% endmacro %}
{% set enumName %}{{param.enumName|default:"StoryboardSegue"}}{% endset %}
{% set unnamedSegue %}{{param.unnamedSegueCaseName|default:"unnamedSegue"}}{% endset %}
{% for scene in customSceneTypes where scene.segues %}
{% set sceneClass %}{% call className scene %}{% endset %}
{{accessModifier}} extension {{sceneClass}} {
{{accessModifier}} enum {{enumName}}: String {
{% for segue in scene.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
}

{{accessModifier}} func perform(segue: {{enumName}}, sender: Any? = nil) {
performSegue(withIdentifier: segue.rawValue, sender: sender)
}

{{accessModifier}} enum Typed{{enumName}} {
{% set hasUnnamedSegue %}{% for segue in scene.segues where not scene.identifier %}1{% endfor %}{% endset %}
{% for segue in scene.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% filter removeNewlines:"leading" %}
{% if segue.customClass or segue.destination %}(
{% if segue.destination %}destination: {% call className segue.destination %}{% endif %}
{% if segue.customClass and segue.destination %}, {% endif %}
{% if segue.customClass %}segue: {% call className segue %}{% endif %}
){% endif %}
{% endfilter %}
{% endfor %}
{% if hasUnnamedSegue %}
case {{unnamedSegue}}
{% endif %}

// swiftlint:disable cyclomatic_complexity
init(segue: {{prefix}}StoryboardSegue) {
switch segue.identifier ?? "" {
{% for segue in scene.segues where segue.identifier %}
case "{{segue.identifier}}":
{% if segue.customClass %}
{% set segueClass %}{% call className segue %}{% endset %}
guard let segue = segue as? {{segueClass}} else {
fatalError("Segue '{{segue.identifier}}' is not of the expected type {{segueClass}}.")
}
{% endif %}
{% if segue.destination %}
{% set destinationClass %}{% call className segue.destination %}{% endset %}
{% if destinationClass != "UIKit.UIViewController" %}
guard let vc = segue.destination{% if isAppKit %}Controller{% endif %} as? {{destinationClass}} else {
fatalError("Destination of segue '{{segue.identifier}}' is not of the expected type {{destinationClass}}.")
}
{% else %}
let vc = segue.destination{% if isAppKit %}Controller{% endif %}
{% endif %}
{% endif %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
self = .{{segueID|escapeReservedKeywords}}{% filter removeNewlines:"leading" %}
{% if segue.customClass or segue.destination %}(
{% if segue.destination %}destination: vc{% endif %}
{% if segue.customClass and segue.destination %}, {% endif %}
{% if segue.customClass %}segue: segue{% endif %}
){%endif %}
{% endfilter %}
{% endfor %}
{% if hasUnnamedSegue %}
case "":
self = .{{unnamedSegue}}
{% endif %}
default:
fatalError("Unrecognized segue '\(segue.identifier ?? "")' in {{sceneClass}}")
}
}
// swiftlint:enable cyclomatic_complexity
}
}

{% endfor %}
{{accessModifier}} enum {{enumName}} {
{% for storyboard in storyboards where storyboard.segues %}
{{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
{% for segue in storyboard.segues %}
{% for segue in storyboard.segues where segue.identifier %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
Expand Down
77 changes: 77 additions & 0 deletions Sources/SwiftGenKit/Parsers/InterfaceBuilder/CustomType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// SwiftGenKit
// Copyright © 2020 SwiftGen
// MIT Licence
//

import Foundation

extension InterfaceBuilder {
struct CustomType: InterfaceBuilderSwiftType {
let type: String
let module: String?
let moduleIsPlaceholder: Bool
var segues = Set<Segue>()
var destinations = [Segue: Scene]()

init(scene: Scene) {
type = scene.type
module = scene.moduleIsPlaceholder ? "<placeholder>" : scene.module
moduleIsPlaceholder = scene.moduleIsPlaceholder
}

mutating func add(segue: Segue, destination: Scene?, warningHandler: Parser.MessageHandler?) {
segues.insert(segue)

// store destination, or check it's at least the same type
if let destination = destination {
if let current = destinations[segue] {
if !areEqual(current, destination) {
let message = "warning: The segue with identifier '\(segue.identifier)' in \(module ?? "").\(type) has " +
"multiple destination types: \(destination.module ?? "").\(destination.type) and " +
"\(destination.module ?? "").\(destination.type)."
warningHandler?(message, #file, #line)
}
} else {
destinations[segue] = destination
}
}
}

private func areEqual(_ lhs: Scene, _ rhs: Scene) -> Bool {
lhs.tag != rhs.tag ||
lhs.customClass != rhs.customClass ||
lhs.customModule != rhs.customModule
}
}
}

extension InterfaceBuilder.Parser {
var customSceneTypes: [InterfaceBuilder.CustomType] {
var result: [String: InterfaceBuilder.CustomType] = [:]

// collect scenes by custom type
for storyboard in storyboards {
for scene in storyboard.scenes where scene.customClass != nil {
let key = InterfaceBuilder.Parser.identifier(for: scene)
var customType = result[key] ?? InterfaceBuilder.CustomType(scene: scene)

for segue in scene.segues {
let destination = self.destination(for: segue.destination, in: storyboard)
customType.add(segue: segue, destination: destination, warningHandler: warningHandler)
}

result[key] = customType
}
}

return result
.sorted { $0.0 < $1.0 }
.map { _, value in value }
}

private static func identifier(for scene: InterfaceBuilder.Scene) -> String {
let module = scene.moduleIsPlaceholder ? "<placeholder>" : scene.module
return "\(module ?? "").\(scene.type)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,26 @@ public enum InterfaceBuilder {
return platforms.first
}
}

func destination(for sceneID: String, in storyboard: Storyboard) -> Scene? {
// directly to a scene
if let scene = storyboard.scenes.first(where: { $0.sceneID == sceneID }) {
return scene
}

// to a scene placeholder
if let placeholder = storyboard.placeholders.first(where: { $0.sceneID == sceneID }),
let storyboard = storyboards.first(where: { $0.name == placeholder.storyboardName }) {
// can be either a scene by identifier, or the initial scene
if let referencedIdentifier = placeholder.referencedIdentifier,
let scene = storyboard.scenes.first(where: { $0.identifier == referencedIdentifier }) {
return scene
} else if placeholder.referencedIdentifier == nil {
return storyboard.initialScene
}
}

return nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// SwiftGenKit
// Copyright © 2020 SwiftGen
// MIT Licence
//

import Foundation

protocol InterfaceBuilderSwiftType {
var type: String { get }
var module: String? { get }
var moduleIsPlaceholder: Bool { get }
}
Loading