$30 off During Our Annual Pro Sale. View Details »

Integrate your app to modern world

d_date
September 21, 2019

Integrate your app to modern world

d_date

September 21, 2019
Tweet

More Decks by d_date

Other Decks in Programming

Transcript

  1. Integrate your app
    to modern world
    iPlayground 2019
    Daiki Matsudate
    @d_date
    iOS Developer

    View Slide

  2. Daiki Matsudate
    • Tokyo


    • iOS Developer from iOS 4

    • Google Developers Expert for Firebase

    • Independent Developer

    • Sushi


    • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ

    View Slide

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

    View Slide

  4. Released iOS 13

    View Slide

  5. iPhone 11 Pro

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

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

    View Slide

  10. The Age of Declarative UI

    View Slide

  11. The Age of Declarative UI
    • iOS: SwiftUI

    • Android: Jetpack Compose

    • ReactNative / Flutter

    View Slide

  12. SwiftUI
    • UI Framework with declarative Syntax

    • Using newest Swift features

    • Property wrapper

    • Function Builder

    • Opaque Result Type

    View Slide

  13. Available in iOS 13

    View Slide

  14. Ready for SwiftUI

    View Slide

  15. Small components

    View Slide

  16. Small Components
    • Use StackView as possible

    • Use Xib as possible

    • Use UIViewController than UIView

    View Slide

  17. Small Components
    • Use StackView as possible

    • Use Xib as possible

    • Use UIViewController than UIView
    super.init(nibName: nil, bundle: nil)

    View Slide

  18. Ex. Compositional Layout
    • Featured content

    • Other sections

    • The order will be A/B testing

    View Slide

  19. Horizontal CollectionView
    Vertical CollectionView
    Ex. Compositional Layout

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. 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

    View Slide

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

    View Slide

  25. 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

    View Slide

  26. Ex. User Registration
    • Each form need to be validate

    • Enable to tap “Done” when all form has valid

    • Show error below each form

    View Slide

  27. Ex. User Registration
    Vertical Stack view
    FormViewController
    FormViewController
    FormViewController
    FormViewController

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Stack in horizontal
    HStack!

    View Slide

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

    View Slide

  33. 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

    View Slide

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

    View Slide

  35. 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

    View Slide

  36. Gather validation results
    How to be enabled?

    View Slide

  37. 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

    View Slide

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

    View Slide

  39. 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)

    View Slide

  40. Gather validation results
    Enabled

    View Slide

  41. Gather validation results
    Error
    Disabled

    View Slide

  42. View Slide

  43. View Slide

  44. 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

    View Slide

  45. 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

    View Slide

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

    View Slide

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

    View Slide

  48. FormViewController.xib

    View Slide

  49. FormViewController.xib
    VStack
    Title
    Text Field
    Error Label

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. 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

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

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

    View Slide

  57. 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

    View Slide

  58. FormView preview

    View Slide

  59. Dataflow

    View Slide

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

    View Slide

  61. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  66. Demo

    View Slide

  67. Doesn’t work well

    View Slide

  68. UIKit in SwiftUI

    View Slide

  69. UIKit in SwiftUI
    • Use UIViewControllerRepresentable / UIViewRepresentable

    • Call in SwiftUI View

    View Slide

  70. 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() -> () {
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  73. 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

    View Slide

  74. Demo

    View Slide

  75. Automatic Preview with UIHostController

    View Slide

  76. Failed to load xib inside SwiftUI

    View Slide

  77. Time to throw away xib

    View Slide

  78. Use Stack view as possible

    View Slide

  79. 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

    View Slide

  80. Use stack view instead of xib

    View Slide

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

    View Slide