Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Integrate your app to modern world in Niigata

d_date
October 11, 2019

Integrate your app to modern world in Niigata

2019/10/11 Mobile Crew Niigata

d_date

October 11, 2019
Tweet

More Decks by d_date

Other Decks in Programming

Transcript

  1. Integrate your app
    to modern world
    Mobile Crew NIIGATA
    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. Daiki Matsudate
    • Tokyo

    • iOS Developer from iOS 4

    • Google Developers Expert for Firebase

    • Independent Developer

    • Sushi

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

    View Slide

  4. Daiki Matsudate
    • Tokyo

    • iOS Developer from iOS 4

    • Google Developers Expert for Firebase

    • Independent Developer

    • Sushi

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

    View Slide

  5. View Slide

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

    View Slide

  7. Released iOS 13

    View Slide

  8. Released iOS 13.1

    View Slide

  9. Released iOS 13.2 Beta 2

    View Slide

  10. iPhone 11 Pro

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

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

    View Slide

  15. The Age of Declarative UI

    View Slide

  16. The Age of Declarative UI
    • iOS: SwiftUI

    • Android: Jetpack Compose

    • ReactNative / Flutter

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  35. Available in iOS 13

    View Slide

  36. Ready for SwiftUI

    View Slide

  37. Small components
    Key Concept

    View Slide

  38. Model View Controller

    View Slide

  39. Massive View Controller

    View Slide

  40. Massive View Controller
    • Business logic in View Controller


    View Slide

  41. Massive View Controller
    • Business logic in View Controller

    • Many UI Components in one View Controller

    View Slide

  42. Small Components
    • Use StackView as possible

    • Use Xib as possible

    • Use UIViewController than UIView

    View Slide

  43. Small Components
    • Use StackView as possible

    • Use Xib as possible

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

    View Slide

  44. Ex. Compositional Layout
    • Featured content

    • Other sections

    • The order will be A/B testing

    View Slide

  45. Horizontal CollectionView
    Vertical CollectionView
    Ex. Compositional Layout

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. Stack in horizontal
    HStack!

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  64. Gather validation results
    How to be enabled?

    View Slide

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

  66. Gather validation results
    Enabled

    View Slide

  67. Gather validation results
    Error
    Disabled

    View Slide

  68. View Slide

  69. View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  74. FormViewController.xib

    View Slide

  75. FormViewController.xib
    VStack
    Title
    Text Field
    Error Label

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  84. FormView preview

    View Slide

  85. Dataflow

    View Slide

  86. Combine

    View Slide

  87. Combine
    • Declarative Swift API

    • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ

    • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά

    • Reactive Framework by Apple

    View Slide

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

    View Slide

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

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

    View Slide

  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
    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  95. UIKit in SwiftUI

    View Slide

  96. UIKit in SwiftUI
    • Use UIViewControllerRepresentable / UIViewRepresentable

    • Call in SwiftUI View

    View Slide

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

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

    View Slide

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

    View Slide

  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

    View Slide

  101. Automatic Preview with UIHostController

    View Slide

  102. Failed to load xib inside SwiftUI

    View Slide

  103. Time to throw away xib

    View Slide

  104. Use Stack view as possible

    View Slide

  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

    View Slide

  106. Use stack view instead of xib

    View Slide

  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

    View Slide