Integrate your app to modern world in Niigata

2594ac7ce91fd7d9a3ce71ca7cc2d0c0?s=47 d_date
October 11, 2019

Integrate your app to modern world in Niigata

2019/10/11 Mobile Crew Niigata

2594ac7ce91fd7d9a3ce71ca7cc2d0c0?s=128

d_date

October 11, 2019
Tweet

Transcript

  1. Integrate your app to modern world Mobile Crew NIIGATA Daiki

    Matsudate @d_date iOS Developer
  2. Daiki Matsudate • Tokyo • iOS Developer from iOS 4

    • Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
  3. Daiki Matsudate • Tokyo • iOS Developer from iOS 4

    • Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
  4. Daiki Matsudate • Tokyo • iOS Developer from iOS 4

    • Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
  5. None
  6. March, 18 - 20th, 2020 https://www.tryswift.co/

  7. Released iOS 13

  8. Released iOS 13.1

  9. Released iOS 13.2 Beta 2

  10. iPhone 11 Pro

  11. None
  12. None
  13. None
  14. https://twitter.com/ios_memes/status/1174273871983370240?s=21

  15. The Age of Declarative UI

  16. The Age of Declarative UI • iOS: SwiftUI • Android:

    Jetpack Compose • ReactNative / Flutter
  17. SwiftUI • UI Framework with declarative Syntax • Live Preview

    in Xcode • Available for iOS, iPadOS, macOS, watchOS and tvOS • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type • Goodbye Storyboard / Xib
  18. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  19. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  20. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif public protocol View { associatedtype Body : View var body: Self.Body { get } }
  21. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  22. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  23. import SwiftUI struct SpeakerList: View { var body: View {

    NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  24. import SwiftUI struct SpeakerList: View { var body: NavigationView<List<Speaker.ID, NavigationLink<SpeakerRow,

    SpeakerDetail>>> { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  25. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Opaque Result Type
  26. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  27. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
  28. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif No Comma!
  29. import SwiftUI struct SpeakerList: View { var body: some View

    { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Function Builders
  30. struct ContentView: View { @State var selectedIndex: Int = 0

    var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
  31. struct ContentView: View { @State var selectedIndex: Int = 0

    var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
  32. struct ContentView: View { @State var selectedIndex: Int = 0

    var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper wrappedValue: Value projectedValue: Wrapped<Value> Without $ With $
  33. struct ContentView: View { @State var selectedIndex: Int = 0

    var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper Value Binding<Value> Without $ With $ cf. RxSwift.BehaviorRelay
  34. SwiftUI • UI Framework with declarative Syntax • Live Preview

    in Xcode • Available for iOS, iPadOS, macOS, watchOS and tvOS • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type
  35. Available in iOS 13

  36. Ready for SwiftUI

  37. Small components Key Concept

  38. Model View Controller

  39. Massive View Controller

  40. Massive View Controller • Business logic in View Controller •

  41. Massive View Controller • Business logic in View Controller •

    Many UI Components in one View Controller
  42. Small Components • Use StackView as possible • Use Xib

    as possible • Use UIViewController than UIView
  43. Small Components • Use StackView as possible • Use Xib

    as possible • Use UIViewController than UIView super.init(nibName: nil, bundle: nil)
  44. Ex. Compositional Layout • Featured content • Other sections •

    The order will be A/B testing
  45. Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

  46. Vertical Stack view Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

  47. VStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak

    self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.widthAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } }
  48. VStackViewController import UIKit open class VStackViewController: UIViewController { public let

    scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) }
  49. VStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak

    self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.widthAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } } Set Constraints all edges of view and width anchor
  50. import UIKit final class HomeViewController: VStackViewController { private let featuredComponent

    = HomeFeaturedCellViewController(dependencies: .init(onTap: { index in // transition logic })) private let rankingComponents = HomeRankingViewController() init() { super.init(components: [featuredComponent, rankingComponents]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = "Home" }
  51. import UIKit final class HomeViewController: VStackViewController { private let featuredComponent

    = HomeFeaturedCellViewController(dependencies: .init(onTap: { index in // transition logic })) private let rankingComponents = HomeRankingViewController() init() { super.init(components: [featuredComponent, rankingComponents]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = "Home" } Override initializer with child components
  52. Ex. User Registration • Each form need to be validate

    • Enable to tap “Done” when all form has valid • Show error below each form
  53. Ex. User Registration Vertical Stack view FormViewController FormViewController FormViewController FormViewController

  54. FormViewController class FormViewController: UIViewController { struct Dependency { let title:

    String let placeholder: String let validation: (String) -> ValidationResult let textContentType: UITextContentType? let keyboardType: UIKeyboardType? let isSecureTextEntry: Bool init(title: String, placeholder: String, validation: @escaping (String) -> ValidationResult, textContentType: UITextContentType? = nil, keyboardType: UIKeyboardType? = nil, isSecureTextEntry: Bool = false) { self.title = title self.placeholder = placeholder self.validation = validation self.textContentType = textContentType self.keyboardType = keyboardType self.isSecureTextEntry = isSecureTextEntry } }
  55. FormViewController @IBOutlet private var titleLabel: UILabel! @IBOutlet var textField: UITextField!

    @IBOutlet private var errorLabel: UILabel! private let dependency: Dependency private let disposeBag = DisposeBag() private lazy var viewModel: FormViewModel = .init(text: self.textField.rx.text.orEmpty.asDriver(), validation: self.dependency.validation) var isValid: Driver<Bool> { return viewModel.validatedValue.map { $0.isValid } } init(dependency: Dependency) { self.dependency = dependency super.init(nibName: nil, bundle: nil) }
  56. FormViewController override func viewDidLoad() { super.viewDidLoad() errorLabel.isHidden = true titleLabel.text

    = dependency.title textField.placeholder = dependency.placeholder textField.textContentType = dependency.textContentType if let keyboardType = dependency.keyboardType { textField.keyboardType = keyboardType } viewModel.validatedValue .drive(errorLabel.rx.validationResult) .disposed(by: disposeBag) }
  57. Stack in horizontal HStack!

  58. HStackViewController import UIKit open class HStackViewController: UIViewController { public let

    scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) }
  59. HStackViewController import UIKit open class HStackViewController: UIViewController { public let

    scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) } Set to horizontal Scroll View Own child view controllers
  60. HStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak

    self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.heightAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } }
  61. HStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak

    self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.heightAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } } Set Constraints all edges of view and height anchor
  62. final class RegisterContentViewController: VStackViewController { private let userNameComponent = FormViewController(

    dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) ) private let emailComponent = FormViewController( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) ) Initialize components
  63. init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:

    [expirationComponent, securityCodeComponent] ) ] ) } Initialize components
  64. Gather validation results How to be enabled?

  65. lazy var isValidateForms: Driver<Bool> = Driver.combineLatest( userNameComponent.isValid, emailComponent.isValid, creditCardComponent.isValid, securityCodeComponent.isValid,

    expirationComponent.isValid ) { $0 && $1 && $2 && $3 && $4 } .distinctUntilChanged() Passing results isValidateForms .drive(onNext: { [weak self] in guard let self = self else { return } self.confirmViewController.set(isValid: $0) }) .disposed(by: disposeBag)
  66. Gather validation results Enabled

  67. Gather validation results Error Disabled

  68. None
  69. None
  70. final class RegisterContentViewController: VStackViewController { private let userNameComponent = FormViewController(

    dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) ) private let emailComponent = FormViewController( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) ) Initialize components in UIKit
  71. final class RegisterContentViewController: VStackViewController { private let userNameView = UIHostingController(

    rootView: FormView( dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) )) private let emailView = UIHostingController( rootView: FormView( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) )) Initialize components in SwiftUI
  72. init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:

    [expirationComponent, securityCodeComponent] ) ] ) } Initialize components in UIKit
  73. init() { super.init( components: [ userNameView, emailView, creditCardView, HStackViewController( components:

    [expirationView, securityCodeView] ) ] ) } Initialize components in SwiftUI
  74. FormViewController.xib

  75. FormViewController.xib VStack Title Text Field Error Label

  76. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI
  77. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI For Light / Dark mode
  78. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Stack
  79. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Title Label
  80. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Text Field
  81. var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack

    { HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Error label
  82. FormView preview struct FormView_Previews: PreviewProvider { static var previews: some

    View { let form = FormView(dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress)) return Group { form.environment(\.colorScheme, .light) form.environment(\.colorScheme, .dark) } .previewLayout(.fixed(width: 414, height: 129)) } }
  83. FormView preview struct FormView_Previews: PreviewProvider { static var previews: some

    View { let form = FormView(dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress)) return Group { form.environment(\.colorScheme, .light) form.environment(\.colorScheme, .dark) } .previewLayout(.fixed(width: 414, height: 129)) } } Light Mode Dark Mode
  84. FormView preview

  85. Dataflow

  86. Combine

  87. Combine • Declarative Swift API • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά •

    Reactive Framework by Apple
  88. https://twitter.com/diegopetrucci/status/1135655480825655297

  89. User Interaction SwiftUI Action State Mutation View Updates Render !

    " ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/
  90. Dataflow in FormView struct FormView: View { let dependency: FormViewController.Dependency

    @ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty }
  91. Dataflow in FormView struct FormView: View { let dependency: FormViewController.Dependency

    @ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty } ViewModel with ObservedObject
  92. Dataflow in FormView import Foundation import SwiftUI class FormViewSwiftUIModel: ObservableObject

    { let validation: (String) -> ValidationResult var value: String = "" { willSet { if newValue != value { validationResult = self.validation(newValue) } } } var validationResult: ValidationResult = .empty { willSet { objectWillChange.send() } }
  93. Dataflow in FormView var validationResult: ValidationResult = .empty { willSet

    { switch newValue { case .failed(let message): errorMessage = message case .ok: isValid = true isEmpty = false case .empty: isValid = false isEmpty = true default: errorMessage = "" } objectWillChange.send() } }
  94. https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()

  95. UIKit in SwiftUI

  96. UIKit in SwiftUI • Use UIViewControllerRepresentable / UIViewRepresentable • Call

    in SwiftUI View
  97. UIKit in SwiftUI extension FormViewController: UIViewControllerRepresentable { typealias UIViewControllerType =

    FormViewController func makeUIViewController( context: UIViewControllerRepresentableContext<FormViewController>) -> FormViewController { let formViewController = FormViewController(dependency: self.dependency) return formViewController } func updateUIViewController(_ uiViewController: FormViewController, context: UIViewControllerRepresentableContext<FormViewController>) { } func makeCoordinator() -> () { } }
  98. UIKit in SwiftUI var body: some View { ZStack {

    Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack { userNameComponent emailComponent creditCardComponent HStack { expirationComponent securityCodeComponent Spacer() } Spacer() } } }
  99. UIKit in SwiftUI var body: some View { ZStack {

    Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack { userNameComponent emailComponent creditCardComponent HStack { expirationComponent securityCodeComponent Spacer() } Spacer() } } } super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components: [expirationComponent, securityCodeComponent] )] )
  100. struct RegisterContentView_Previews: PreviewProvider { static var previews: some View {

    let content = RegisterContentView() return Group { NavigationView { content.environment(\.colorScheme, .light) } NavigationView { content.environment(\.colorScheme, .dark) } } } } UIKit in SwiftUI - Preview
  101. Automatic Preview with UIHostController

  102. Failed to load xib inside SwiftUI

  103. Time to throw away xib

  104. Use Stack view as possible

  105. let stackView = UIStackView(arrangedSubviews: [titleLabel, textField, errorLabel]) stackView.axis = .vertical

    stackView.spacing = 16 view.addSubview(stackView, constraints: .allEdges(margin: 16)) Use stack view instead of xib
  106. Use stack view instead of xib

  107. Recap - UIKit to SwiftUI • Use stack view as

    possible • Some of SwiftUI feature still be broken • Sometime need to use UIKit instead • Keeping components small is key factor to migrate