Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Integrate your app to modern world
Search
d_date
September 21, 2019
Programming
2
620
Integrate your app to modern world
iPlayground 2019
Example:
https://github.com/d-date/IntegrateAppExample
d_date
September 21, 2019
Tweet
Share
More Decks by d_date
See All by d_date
TCA Practice in 5 min
d_date
2
1.1k
waiwai-swiftpm-part2
d_date
3
460
わいわいSwift PM part 1
d_date
2
370
What's new in Firebase 2021
d_date
2
1.4k
CI/CDをミニマルに構築する
d_date
1
530
Swift Package centered project - Build and Practice
d_date
20
13k
How to write Great Proposal
d_date
4
1.1k
Thinking about Architecture for SwiftUI
d_date
8
2.3k
Integrate your app to modern world in Niigata
d_date
0
620
Other Decks in Programming
See All in Programming
Duckdb-Wasmでローカルダッシュボードを作ってみた
nkforwork
0
130
Contemporary Test Cases
maaretp
0
140
Arm移行タイムアタック
qnighy
0
330
「今のプロジェクトいろいろ大変なんですよ、app/services とかもあって……」/After Kaigi on Rails 2024 LT Night
junk0612
5
2.2k
型付き API リクエストを実現するいくつかの手法とその選択 / Typed API Request
euxn23
8
2.2k
macOS でできる リアルタイム動画像処理
biacco42
9
2.4k
CSC509 Lecture 11
javiergs
PRO
0
180
카카오페이는 어떻게 수천만 결제를 처리할까? 우아한 결제 분산락 노하우
kakao
PRO
0
110
Streams APIとTCPフロー制御 / Web Streams API and TCP flow control
tasshi
2
350
Jakarta EE meets AI
ivargrimstad
0
650
最新TCAキャッチアップ
0si43
0
190
[Do iOS '24] Ship your app on a Friday...and enjoy your weekend!
polpielladev
0
110
Featured
See All Featured
Reflections from 52 weeks, 52 projects
jeffersonlam
346
20k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
250
21k
Side Projects
sachag
452
42k
Gamification - CAS2011
davidbonilla
80
5k
A better future with KSS
kneath
238
17k
Building an army of robots
kneath
302
43k
Optimizing for Happiness
mojombo
376
70k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
28
2k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
8
890
Making Projects Easy
brettharned
115
5.9k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
329
21k
BBQ
matthewcrist
85
9.3k
Transcript
Integrate your app to modern world iPlayground 2019 Daiki Matsudate
@d_date iOS Developer
Daiki Matsudate • Tokyo • iOS Developer from iOS 4
• Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖʯ
March, 18 - 20th, 2020 https://www.tryswift.co/
Released iOS 13
iPhone 11 Pro
None
None
None
https://twitter.com/ios_memes/status/1174273871983370240?s=21
The Age of Declarative UI
The Age of Declarative UI • iOS: SwiftUI • Android:
Jetpack Compose • ReactNative / Flutter
SwiftUI • UI Framework with declarative Syntax • Using newest
Swift features • Property wrapper • Function Builder • Opaque Result Type
Available in iOS 13
Ready for SwiftUI
Small components
Small Components • Use StackView as possible • Use Xib
as possible • Use UIViewController than UIView
Small Components • Use StackView as possible • Use Xib
as possible • Use UIViewController than UIView super.init(nibName: nil, bundle: nil)
Ex. Compositional Layout • Featured content • Other sections •
The order will be A/B testing
Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout
Vertical Stack view Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout
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) }
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) } }
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
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" }
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
Ex. User Registration • Each form need to be validate
• Enable to tap “Done” when all form has valid • Show error below each form
Ex. User Registration Vertical Stack view FormViewController FormViewController FormViewController FormViewController
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 } }
FormViewController @IBOutlet private var titleLabel: UILabel! @IBOutlet var textField: UITextField!
@IBOutlet private var errorLabel: UILabel! private let dependency: Dependency private let disposeBag = DisposeBag() private lazy var viewModel: FormViewModel = .init(text: self.textField.rx.text.orEmpty.asDriver(), validation: self.dependency.validation) var isValid: Driver<Bool> { return viewModel.validatedValue.map { $0.isValid } } init(dependency: Dependency) { self.dependency = dependency super.init(nibName: nil, bundle: nil) }
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) }
Stack in horizontal HStack!
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) }
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
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) } }
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
Gather validation results How to be enabled?
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
init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:
[expirationComponent, securityCodeComponent] ) ] ) } Initialize components
lazy var isValidateForms: Driver<Bool> = Driver.combineLatest( userNameComponent.isValid, emailComponent.isValid, creditCardComponent.isValid, securityCodeComponent.isValid,
expirationComponent.isValid ) { $0 && $1 && $2 && $3 && $4 } .distinctUntilChanged() Passing results isValidateForms .drive(onNext: { [weak self] in guard let self = self else { return } self.confirmViewController.set(isValid: $0) }) .disposed(by: disposeBag)
Gather validation results Enabled
Gather validation results Error Disabled
None
None
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
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
init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:
[expirationComponent, securityCodeComponent] ) ] ) } Initialize components in UIKit
init() { super.init( components: [ userNameView, emailView, creditCardView, HStackViewController( components:
[expirationView, securityCodeView] ) ] ) } Initialize components in SwiftUI
FormViewController.xib
FormViewController.xib VStack Title Text Field Error Label
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
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
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
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
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
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
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)) } }
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
FormView preview
Dataflow
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 }
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
https://developer.apple.com/videos/play/wwdc2019/226/
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() } }
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() } }
https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()
Demo
Doesn’t work well
UIKit in SwiftUI
UIKit in SwiftUI • Use UIViewControllerRepresentable / UIViewRepresentable • Call
in SwiftUI View
UIKit in SwiftUI extension FormViewController: UIViewControllerRepresentable { typealias UIViewControllerType =
FormViewController func makeUIViewController( context: UIViewControllerRepresentableContext<FormViewController>) -> FormViewController { let formViewController = FormViewController(dependency: self.dependency) return formViewController } func updateUIViewController(_ uiViewController: FormViewController, context: UIViewControllerRepresentableContext<FormViewController>) { } func makeCoordinator() -> () { } }
UIKit in SwiftUI var body: some View { ZStack {
Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack { userNameComponent emailComponent creditCardComponent HStack { expirationComponent securityCodeComponent Spacer() } Spacer() } } }
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] )] )
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
Demo
Automatic Preview with UIHostController
Failed to load xib inside SwiftUI
Time to throw away xib
Use Stack view as possible
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
Use stack view instead of xib
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