РИТ++ 2017, App's Conf
Зал Найроби, 6 июня, 10:00
Тезисы:
http://appsconf.ru/2017/abstracts/2818.html
Ни для кого не секрет, что Swift — это mainstream: его активно продвигает Apple, на нем пишутся все новые фреймворки, многие разработчики начинают именно с него. Но так ли просто мигрировать c Objective-С, если твоему приложению 5 лет и оно имеет большую аудиторию? В докладе мы расскажем о том, как сделать это без ущерба для бизнеса.
Вы узнаете об этапах такого перехода:
1. Какую бизнес-проблему решали? - Ускоряем разработку, уменьшаем количество багов, проще и быстрее находим новых сотрудников, ограждаем от будущих рисков (старых не поддерживаемых фреймворков, устаревших АПИ).
...
22. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Новые библиотеки и UI компоненты пишутся на Swift
23. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Новые библиотеки и UI компоненты пишутся на Swift
• Старые Objc библиотеки не поддерживаются
24. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Новые библиотеки и UI компоненты пишутся на Swift
• Старые Objc библиотеки не поддерживаются
NOTE: This is legacy introduction to the Objective-C ReactiveCocoa,
pod 'ReactiveCocoa', '~> 2.5.0'
25. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Поиск новых сотрудников
26. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Поиск новых сотрудников
• 70% собеседуемых хотели бы писать на swift
27. Зачем решили переходить?
• Более предсказуемый код
• Заблаговременное мигрирование с objc библиотек
• Поиск новых сотрудников
• 70% собеседуемых хотели бы писать на swift
• После перехода на swift удалось найти
29. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
30. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• поддержка того же уровня crash-free
31. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• поддержка того же уровня crash-free
• такая же скорость разработки
32. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
33. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• 5 лет разработки
34. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• 5 лет разработки
• разные архитектурные патерны
35. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• Хочется избежать кучи “граблей”, чтобы потом
все снова не переписывать
36. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• Хочется избежать кучи “граблей”, чтобы потом
все снова не переписывать
• архитектура swift ориентированная
37. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• Хочется избежать кучи “граблей”, чтобы потом
все снова не переписывать
• архитектура swift ориентированная
• тестируемость
38. Но не так все просто
• Бизнес хочет постоянную поставку новых фич
• Приложения имею долгую историю
• Хочется избежать кучи “граблей”, чтобы потом
все снова не переписывать
• архитектура swift ориентированная
• тестируемость
• кодстай и т.д
45. Nullability
• 3 месяца на каждый измененный файл добавляем Nullability
• Скрипт который не дает пройти PullRequest без меток :)
Как улучшить показатель?
46. Nullability
• 3 месяца на каждый измененный файл добавляем Nullability
• Скрипт который не дает пройти PullRequest без меток :)
60%
Как улучшить показатель?
57. profileFacade.authorize(withLogin: login,
password: pass).doNext(block: ((Any?) -> Void)!)
profileFacade
.authorize(withLogin: login, password: pass)
.subscribeNext { (response) in
if let user = response as? SJAProfileModel {
print("(String(describing: user.name))")
}
}
Проблемы
1. Нет понятия, что получаешь
2. Необходимо каждый раз “кастить”
58. profileFacade.authorize(withLogin: login,
password: pass).doNext(block: ((Any?) -> Void)!)
profileFacade
.authorize(withLogin: login, password: pass)
.subscribeNext { (response) in
if let user = response as? SJAProfileModel {
print("(String(describing: user.name))")
}
}
Проблемы
1. Нет понятия, что получаешь
2. Необходимо каждый раз “кастить”
60. • Хочется, чтобы можно было легко пользоваться
Критерии для решения
61. • Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
Критерии для решения
62. • Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
• Swift like API
Критерии для решения
63. • Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
• Swift like API
Критерии для решения
64. • Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
• Swift like API
Критерии для решения
65. • Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
• Swift like API
Критерии для решения
66. ?
• Хочется, чтобы можно было легко пользоваться
• Чтобы была строгая типизация
• Swift like API
Критерии для решения
67. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
68. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
69. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
70. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
71. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
72. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
73. extension RACSignal {
private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> {
return Observable.create() { observer in
self.subscribeNext(
{ anyValue in
if let converted = convertBlock(anyValue) {
observer.onNext(converted)
} else {
observer.onError(RxCastError.cannotConvertTypes)
}
},
...
...
...
return Disposables.create() {
}
}
}
+1. Подписываемся на новые значения RACSignal
74. extension RACSignal {
public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> {
return rxMapBody() { anyValue in
if let value: T = rx_cast(anyValue) {
return value
} else {
return nil
}
}
}
}
+2. Приводим не типизированное к типизированному
75. extension RACSignal {
public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> {
return rxMapBody() { anyValue in
if let value: T = rx_cast(anyValue) {
return value
} else {
return nil
}
}
}
}
+2. Приводим не типизированное к типизированному
76. extension RACSignal {
public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> {
return rxMapBody() { anyValue in
if let value: T = rx_cast(anyValue) {
return value
} else {
return nil
}
}
}
}
+2. Приводим не типизированное к типизированному
77. extension RACSignal {
public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> {
return rxMapBody() { anyValue in
if let value: T = rx_cast(anyValue) {
return value
} else {
return nil
}
}
}
}
+2. Приводим не типизированное к типизированному
78. extension RACSignal {
public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> {
return rxMapBody() { anyValue in
if let value: T = rx_cast(anyValue) {
return value
} else {
return nil
}
}
}
}
+2. Приводим не типизированное к типизированному
79. +
internal func rx_cast<T>(_ value: Any?) -> T? {
if let v = value as? T {
return v
} else if let E = T.self as? ExpressibleByNilLiteral.Type {
return E.init(nilLiteral: ()) as? T
}
return nil
}
3. “Кастим” к нужному типу T
80. +
internal func rx_cast<T>(_ value: Any?) -> T? {
if let v = value as? T {
return v
} else if let E = T.self as? ExpressibleByNilLiteral.Type {
return E.init(nilLiteral: ()) as? T
}
return nil
}
3. “Кастим” к нужному типу T
81. +
internal func rx_cast<T>(_ value: Any?) -> T? {
if let v = value as? T {
return v
} else if let E = T.self as? ExpressibleByNilLiteral.Type {
return E.init(nilLiteral: ()) as? T
}
return nil
}
3. “Кастим” к нужному типу T
82. +
internal func rx_cast<T>(_ value: Any?) -> T? {
if let v = value as? T {
return v
} else if let E = T.self as? ExpressibleByNilLiteral.Type {
return E.init(nilLiteral: ()) as? T
}
return nil
}
3. “Кастим” к нужному типу T
83. +
internal func rx_cast<T>(_ value: Any?) -> T? {
if let v = value as? T {
return v
} else if let E = T.self as? ExpressibleByNilLiteral.Type {
return E.init(nilLiteral: ()) as? T
}
return nil
}
3. “Кастим” к нужному типу T
95. Не все возможности Swift доступны в
Objective-c
• struct • enum • моки для тестов
96. Не все возможности Swift доступны в
Objective-c
• struct • enum • моки для тестов
?
97. Не все возможности Swift доступны в
Objective-c
• struct • enum • моки для тестов
Sourcery
https://goo.gl/qgnH9D
?
98. Sourcery scans your source code, applies your personal
templates and generates Swift code for you, allowing you to use
meta-programming techniques to save time and decrease
potential mistakes.
99. Sourcery scans your source code, applies your personal
templates and generates Swift code for you, allowing you to use
meta-programming techniques to save time and decrease
potential mistakes.
• equatable/hashable
• NSCoding
• JSON serialization
Автогенерация
100. struct Resume:
AutoHashable,
AutoEquatable {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
extension Resume: Hashable {
internal var hashValue: Int {
return combineHashes([key?.hashValue ?? 0,
name?.hashValue ?? 0,
firstName?.hashValue ?? 0,
lastName?.hashValue ?? 0,
middleName?.hashValue ?? 0,
birthDate?.hashValue ?? 0, 0])
}
}
• Hashable
101. struct Resume:
AutoHashable,
AutoEquatable {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
extension Resume: Hashable {
internal var hashValue: Int {
return combineHashes([key?.hashValue ?? 0,
name?.hashValue ?? 0,
firstName?.hashValue ?? 0,
lastName?.hashValue ?? 0,
middleName?.hashValue ?? 0,
birthDate?.hashValue ?? 0, 0])
}
}
• Hashable
119. struct Resume:
AutoHashable,
AutoEquatable,
AutoObjc {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
Sourcery
Как же это помогло нам?
Struct Objective-c
class ResumeObjc: NSObject {
private(set) var key: Int?
private(set) var name: String?
private(set) var firstName: String?
private(set) var lastName: String?
private(set) var middleName: String?
private(set) var birthDate: Date?
}
120. struct Resume:
AutoHashable,
AutoEquatable,
AutoObjc {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
Sourcery
Как же это помогло нам?
Struct Objective-c
class ResumeObjc: NSObject {
private(set) var key: Int?
private(set) var name: String?
private(set) var firstName: String?
private(set) var lastName: String?
private(set) var middleName: String?
private(set) var birthDate: Date?
}
121. struct Resume:
AutoHashable,
AutoEquatable,
AutoObjc {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
Sourcery
Как же это помогло нам?
Struct Objective-c
class ResumeObjc: NSObject {
private(set) var key: Int?
private(set) var name: String?
private(set) var firstName: String?
private(set) var lastName: String?
private(set) var middleName: String?
private(set) var birthDate: Date?
}
122. struct Resume:
AutoHashable,
AutoEquatable,
AutoObjc {
var key: Int?
let name: String?
var firstName: String?
var lastName: String?
var middleName: String?
var birthDate: Date?
}
Sourcery
Как же это помогло нам?
Struct Objective-c
✓
class ResumeObjc: NSObject {
private(set) var key: Int?
private(set) var name: String?
private(set) var firstName: String?
private(set) var lastName: String?
private(set) var middleName: String?
private(set) var birthDate: Date?
}
127. protocol VacancyDetailViewModelDelegate:
class,
AutoMockable {
func vmReloadData(animated: Bool)
func vmShareItems(items: [Any])
}
SourceryДля тестов нужно самостоятельно
писать моки
class VacancyDetailViewModelDelegateMock:
VacancyDetailViewModelDelegate {
var vmReloadDataCalled = false
var vmReloadDataReceivedAnimated: Bool?
func vmReloadData(animated: Bool) {
vmReloadDataCalled = true
vmReloadDataReceivedAnimated = animated
}
var vmShareItemsCalled = false
var vmShareItemsReceivedItems: [Any]?
func vmShareItems(items: [Any]) {
vmShareItemsCalled = true
vmShareItemsReceivedItems = items
}
}
128. protocol VacancyDetailViewModelDelegate:
class,
AutoMockable {
func vmReloadData(animated: Bool)
func vmShareItems(items: [Any])
}
SourceryДля тестов нужно самостоятельно
писать моки
class VacancyDetailViewModelDelegateMock:
VacancyDetailViewModelDelegate {
var vmReloadDataCalled = false
var vmReloadDataReceivedAnimated: Bool?
func vmReloadData(animated: Bool) {
vmReloadDataCalled = true
vmReloadDataReceivedAnimated = animated
}
var vmShareItemsCalled = false
var vmShareItemsReceivedItems: [Any]?
func vmShareItems(items: [Any]) {
vmShareItemsCalled = true
vmShareItemsReceivedItems = items
}
}
129. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
130. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
143. SwiftGen
• Assets Catalogs
• Localizable.strings
let string = tr(.vacancyCellTitleExpiriens)
let string = L10n.vacancyCellTitleExpiriens.string
144. SwiftGen
• Assets Catalogs
• Localizable.strings
• UIStoryboards and their Scenes
let string = tr(.vacancyCellTitleExpiriens)
let string = L10n.vacancyCellTitleExpiriens.string
145. SwiftGen
• Assets Catalogs
• Localizable.strings
• UIStoryboards and their Scenes
• NSStoryboards and their Scenes
let string = tr(.vacancyCellTitleExpiriens)
let string = L10n.vacancyCellTitleExpiriens.string
146. SwiftGen
• Assets Catalogs
• Localizable.strings
• UIStoryboards and their Scenes
• NSStoryboards and their Scenes
• Colors
let string = tr(.vacancyCellTitleExpiriens)
let string = L10n.vacancyCellTitleExpiriens.string
147. SwiftGen
• Assets Catalogs
• Localizable.strings
• UIStoryboards and their Scenes
• NSStoryboards and their Scenes
• Colors
• Fonts
let string = tr(.vacancyCellTitleExpiriens)
let string = L10n.vacancyCellTitleExpiriens.string
148. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
149. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
✓
150. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
✓
✓
151. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
✓
✓
✓
152. Сели, подумали и сделали
• Много проблем с nullability
• Проблемы со сторонними библиотеками
• Не все возможности swift доступны в Objective-c
• Организационные моменты внутри команды
✓
✓
✓
✓
153. Что нам дал переход
на swift
• Статическая типизация дала более предсказуемое
поведение и меньше багов в бизнес логике
• Более быструю скорость разработки фич
• Наняли людей в команду