Slide 1

Slide 1 text

Integrate your app to modern world iPlayground 2019 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

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

Slide 4

Slide 4 text

Released iOS 13

Slide 5

Slide 5 text

iPhone 11 Pro

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

The Age of Declarative UI

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

SwiftUI • UI Framework with declarative Syntax • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type

Slide 13

Slide 13 text

Available in iOS 13

Slide 14

Slide 14 text

Ready for SwiftUI

Slide 15

Slide 15 text

Small components

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

Slide 20

Slide 20 text

Vertical Stack view Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout

Slide 21

Slide 21 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 22

Slide 22 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 23

Slide 23 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 24

Slide 24 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 25

Slide 25 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 26

Slide 26 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 27

Slide 27 text

Ex. User Registration Vertical Stack view FormViewController FormViewController FormViewController FormViewController

Slide 28

Slide 28 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 29

Slide 29 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 30

Slide 30 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 31

Slide 31 text

Stack in horizontal HStack!

Slide 32

Slide 32 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 33

Slide 33 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 34

Slide 34 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 35

Slide 35 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 36

Slide 36 text

Gather validation results How to be enabled?

Slide 37

Slide 37 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 38

Slide 38 text

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

Slide 39

Slide 39 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 40

Slide 40 text

Gather validation results Enabled

Slide 41

Slide 41 text

Gather validation results Error Disabled

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 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 45

Slide 45 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 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

FormViewController.xib

Slide 49

Slide 49 text

FormViewController.xib VStack Title Text Field Error Label

Slide 50

Slide 50 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 51

Slide 51 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 52

Slide 52 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 53

Slide 53 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 54

Slide 54 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 55

Slide 55 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 56

Slide 56 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 57

Slide 57 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 58

Slide 58 text

FormView preview

Slide 59

Slide 59 text

Dataflow

Slide 60

Slide 60 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 61

Slide 61 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 62

Slide 62 text

https://developer.apple.com/videos/play/wwdc2019/226/

Slide 63

Slide 63 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 64

Slide 64 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 65

Slide 65 text

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

Slide 66

Slide 66 text

Demo

Slide 67

Slide 67 text

Doesn’t work well

Slide 68

Slide 68 text

UIKit in SwiftUI

Slide 69

Slide 69 text

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

Slide 70

Slide 70 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 71

Slide 71 text

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

Slide 72

Slide 72 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 73

Slide 73 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 74

Slide 74 text

Demo

Slide 75

Slide 75 text

Automatic Preview with UIHostController

Slide 76

Slide 76 text

Failed to load xib inside SwiftUI

Slide 77

Slide 77 text

Time to throw away xib

Slide 78

Slide 78 text

Use Stack view as possible

Slide 79

Slide 79 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 80

Slide 80 text

Use stack view instead of xib

Slide 81

Slide 81 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