Slide 1

Slide 1 text

Integrate your app to modern world Mobile Crew NIIGATA Daiki Matsudate @d_date iOS Developer

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

March, 18 - 20th, 2020 https://www.tryswift.co/

Slide 7

Slide 7 text

Released iOS 13

Slide 8

Slide 8 text

Released iOS 13.1

Slide 9

Slide 9 text

Released iOS 13.2 Beta 2

Slide 10

Slide 10 text

iPhone 11 Pro

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

https://twitter.com/ios_memes/status/1174273871983370240?s=21

Slide 15

Slide 15 text

The Age of Declarative UI

Slide 16

Slide 16 text

The Age of Declarative UI • iOS: SwiftUI • Android: Jetpack Compose • ReactNative / Flutter

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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 } }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

import SwiftUI struct SpeakerList: View { var body: NavigationView>> { 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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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!

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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) } } } } }

Slide 31

Slide 31 text

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) } } } } }

Slide 32

Slide 32 text

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 Without $ With $

Slide 33

Slide 33 text

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 Without $ With $ cf. RxSwift.BehaviorRelay

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Available in iOS 13

Slide 36

Slide 36 text

Ready for SwiftUI

Slide 37

Slide 37 text

Small components Key Concept

Slide 38

Slide 38 text

Model View Controller

Slide 39

Slide 39 text

Massive View Controller

Slide 40

Slide 40 text

Massive View Controller • Business logic in View Controller •

Slide 41

Slide 41 text

Massive View Controller • Business logic in View Controller • Many UI Components in one View Controller

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Small Components • Use StackView as possible • Use Xib as possible • Use UIViewController than UIView super.init(nibName: nil, bundle: nil)

Slide 44

Slide 44 text

Ex. Compositional Layout • Featured content • Other sections • The order will be A/B testing

Slide 45

Slide 45 text

Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

Slide 46

Slide 46 text

Vertical Stack view Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

Slide 47

Slide 47 text

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) } }

Slide 48

Slide 48 text

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) }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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" }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Ex. User Registration • Each form need to be validate • Enable to tap “Done” when all form has valid • Show error below each form

Slide 53

Slide 53 text

Ex. User Registration Vertical Stack view FormViewController FormViewController FormViewController FormViewController

Slide 54

Slide 54 text

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 } }

Slide 55

Slide 55 text

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 { return viewModel.validatedValue.map { $0.isValid } } init(dependency: Dependency) { self.dependency = dependency super.init(nibName: nil, bundle: nil) }

Slide 56

Slide 56 text

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) }

Slide 57

Slide 57 text

Stack in horizontal HStack!

Slide 58

Slide 58 text

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) }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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) } }

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components: [expirationComponent, securityCodeComponent] ) ] ) } Initialize components

Slide 64

Slide 64 text

Gather validation results How to be enabled?

Slide 65

Slide 65 text

lazy var isValidateForms: Driver = 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)

Slide 66

Slide 66 text

Gather validation results Enabled

Slide 67

Slide 67 text

Gather validation results Error Disabled

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components: [expirationComponent, securityCodeComponent] ) ] ) } Initialize components in UIKit

Slide 73

Slide 73 text

init() { super.init( components: [ userNameView, emailView, creditCardView, HStackViewController( components: [expirationView, securityCodeView] ) ] ) } Initialize components in SwiftUI

Slide 74

Slide 74 text

FormViewController.xib

Slide 75

Slide 75 text

FormViewController.xib VStack Title Text Field Error Label

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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)) } }

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

FormView preview

Slide 85

Slide 85 text

Dataflow

Slide 86

Slide 86 text

Combine

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

https://twitter.com/diegopetrucci/status/1135655480825655297

Slide 89

Slide 89 text

User Interaction SwiftUI Action State Mutation View Updates Render ! " ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/

Slide 90

Slide 90 text

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 }

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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() } }

Slide 93

Slide 93 text

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() } }

Slide 94

Slide 94 text

https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()

Slide 95

Slide 95 text

UIKit in SwiftUI

Slide 96

Slide 96 text

UIKit in SwiftUI • Use UIViewControllerRepresentable / UIViewRepresentable • Call in SwiftUI View

Slide 97

Slide 97 text

UIKit in SwiftUI extension FormViewController: UIViewControllerRepresentable { typealias UIViewControllerType = FormViewController func makeUIViewController( context: UIViewControllerRepresentableContext) -> FormViewController { let formViewController = FormViewController(dependency: self.dependency) return formViewController } func updateUIViewController(_ uiViewController: FormViewController, context: UIViewControllerRepresentableContext) { } func makeCoordinator() -> () { } }

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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] )] )

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Automatic Preview with UIHostController

Slide 102

Slide 102 text

Failed to load xib inside SwiftUI

Slide 103

Slide 103 text

Time to throw away xib

Slide 104

Slide 104 text

Use Stack view as possible

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

Use stack view instead of xib

Slide 107

Slide 107 text

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