clmct / code-review Goto Github PK
View Code? Open in Web Editor NEWiOS project code review
iOS project code review
Структура проекта
Структура проекта выдержана в рамках заданной архитектуры - это круто) Отметил бы пару незначительных моментов, которые усложняют работу. Особенно, когда проект станет больше.
Модули экранов стоит вынести в общую папку с названием Modules (как вариант)
В основном используется следующая структура
UserCard
UserCardView.swift
UserCardViewModel.swift
В модуле Authentication следующая
viewModel
AuthViewModel.swift
SignInViewModel.swift
SignUpViewModel.swift
view
AuthView.swift
SignInViewController.swift
SignUpViewController.swift
model
AuthValidator.swift
TokenResponse.swift
Я бы придерживался единого стиля, как в первом варианте. Со временем будет тяжело ориентироваться во втором типе структуры
Рекомендую ознакомиться с Структура iOS проекта на медиум. Так же можно посмотреть Удобная структура iOS проекта на хабре. Для закрепления полезно почитать open source код - iOS-Clean-Architecture-MVVM, подчеркунть моменты чистой архитектуры
public class ChatListItemLocal: NSManagedObject {
static func updateOrCreate(fromContest domainItems: [ChatListItem],
context: NSManagedObjectContext) -> [ChatListItemLocal] {
return domainItems.map { domainItem in
return updateOrCreate(from: domainItem, context: context)
}
}
static func updateOrCreate(from domainItem: ChatListItem,
context: NSManagedObjectContext) -> ChatListItemLocal {
let request = ChatListItemLocal.fetchRequest()
request.predicate = NSPredicate(format: "chat.id == \"\(domainItem.chat.id)\"")
guard let localItem = try? context.fetch(request).first else {
return create(from: domainItem, context: context)
}
localItem.update(with: domainItem, context: context)
return localItem
}
Удобное, но опасное решение. Сущность NSManagedObject не должна владеть context DataBase. Это нарушение SRP. Сейчас DataBase выполняет роль proxy класса в плохом смысле этого слова, он перекладывает свою ответственность на сущности CoreData. Database singleton обращается к static полям Database Entities. CoreData должна владеть context и выполнять работу с сущностями. А сейчас в
Архитектура проекта MMVM. Проект выдержан в рамках заданной концепции - это круто) Подсветил бы следующий момент - навигация в ViewController. Сейчас получается, что VC знает о сервисах и хранит ссылки на них, отвечает за навигацию. ViewController не должен отвечать за навигацию и хранить ссылки на сервисы, это нарушение SRP. Сервисы в ViewModel, а навигация в отдельной сущности (координатор). Хорошея статья Принципы SOLID, о которых должен знать каждый разработчик на тему SOLID. За навигацию в проекте должна отвечать отдельная сущность. Я бы предложил вынести эту логику в Router, Assembly (Builder) или Coordinator. Router отвечает за навигацию в приложении в Assembly за сборку модуля. Coordinator же берет на себе роль сборщика и роль навигации. Использование routing позволит переиспользовать сборку и навигацию в коде, делать код менее зависимым и гибким. Подробно о навигации: роутер и координтор.
struct KeyboardInfo {
// MARK: Properties
var animationCurve: UIView.AnimationOptions?
var animationDuration: Double?
var isLocal: Bool?
var frameBegin: CGRect?
var frameEnd: CGRect?
// MARK: Init
init?(_ notification: Notification) {
guard notification.name == UIResponder.keyboardWillShowNotification ||
notification.name == UIResponder.keyboardWillChangeFrameNotification else { return nil }
guard let info = notification.userInfo else { return nil }
animationCurve = info[UIWindow.keyboardAnimationCurveUserInfoKey] as? UIView.AnimationOptions
animationDuration = info[UIWindow.keyboardAnimationDurationUserInfoKey] as? Double
isLocal = info[UIWindow.keyboardIsLocalUserInfoKey] as? Bool
frameBegin = info[UIWindow.keyboardFrameBeginUserInfoKey] as? CGRect
frameEnd = info[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect
}
}
Можно упростить следующий код
guard notification.name == UIResponder.keyboardWillShowNotification ||
notification.name == UIResponder.keyboardWillChangeFrameNotification else { return nil }
guard let info = notification.userInfo else { return nil }
на
guard notification.name == UIResponder.keyboardWillShowNotification ||
notification.name == UIResponder.keyboardWillChangeFrameNotification,
let info = notification.userInfo else { return nil }
Это повысит читаемость кода
В проекте есть Force Unwrap
В целом в проекте не используются плохие практики работы с Optional - force unwrap, код читаемый и безопасный (автор молодей). Подсвечу один момент. В классе AppDelegate и SceneDelegate есть использование force unwrap. Даже когда программист уверен, что не придет nil, глаз может замылиться и проглядеть этот момент - приложение упадет. Рекомендую обратить внимание на линтер кода, который подсветит ошибки на стадии билда. Ознакомиться можно в SwiftLint — чистота и порядок в iOS проекте на хабре.
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let rootViewController = userDefaultManager.tokensExists() ?
TabBarViewController(networkManager: networkManager) :
WelcomeViewController(networkManager: networkManager)
window!.rootViewController = NavigationControllerFactory
.createHiddenNavBarNavigationController(rootViewController: rootViewController)
window!.makeKeyAndVisible()
return true
}
Для наглядности разберем Force Unwrap на примере ниже
var someVariable: String?
var somethingElse: String = "hello"
func setupApp() {
self.somethingElse = self.someVariable!
}
В данном случае someVariable равно Optional. При обращение к переменной приложение упадет Как раз для защиты от таких ошибок и создавались опционалы, а мы сводим все их усилия не нет. Для безопасного получения значения можно использовать:
Optional Binding,
func setupApp() {
if let theThing = someVariable {
self.somethingElse = self.someVariable!
} else {
print("error")
}
}
Optional Chaining
func setupApp() {
self.somethingElse = someClass?.createString()
}
Nil Coalescing
func setupApp() {
self.somethingElse = someVariable ?? "error"
}
Более подробно можно ознакомиться в данной статье Три ошибки iOS-разработчика, которые могут дорого стоить
За использование протоколов плюс в карму, как и за кастомный NavigationController. Подсвечу один момент. В проекте встречается следующий стиль комформить протокол.
protocol NavigationControllerProtocol: UINavigationController {
func setupCommonNavigationBarSettings()
}
class NavigationController: UINavigationController, NavigationControllerProtocol {
func setupCommonNavigationBarSettings() {
navigationBar.backgroundColor = Colors.transparent
navigationBar.shadowImage = UIImage()
navigationBar.tintColor = Colors.pink
navigationBar.barTintColor = Colors.white
navigationBar.titleTextAttributes = [
NSAttributedString.Key.font: UIFont.mediumLabelSemibold,
NSAttributedString.Key.foregroundColor: Colors.pink ?? UIColor.systemGray
]
}
}
Комформить протоколы принято в extension, это повышает читаемость кода.
extension NavigationController: NavigationControllerProtocol {
...
}
Антипаттерн одиночка в мире iOS разработки
class DatabaseService {
// MARK: Properties
static var shared = DatabaseService()
private let context: NSManagedObjectContext
// MARK: Init
private init() {
let container = NSPersistentContainer(name: "CacheDatabase")
container.loadPersistentStores { _, error in
if let error = error {
print(String(describing: error))
}
}
context = container.viewContext
}
// MARK: Private Methods
private func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
context.rollback()
fatalError(String(describing: error))
}
}
}
}
Использование паттерна одиночка (singleton), в том числе для сервисов является плохим тоном и антипаттерном в iOS разработки. Хорошая статья на эту теме - Почему Singleton антипаттерн. Для решения проблемы я бы посоветовал использовать dependency injection принцип. Dependency Injection, или внедрение зависимостей, — это паттерн настройки объекта, при котором зависимости объекта задаются извне, а не создаются самим объектом. Другими словами, объекты настраиваются внешними объектами. Рекомендую ознакомится более подробно DI в iOS: Complete guide
class NetworkService {
// MARK: Properties
private let defaults: UserDefaultsManager
private var isRetrying = false
// MARK: Init
init(defaultsManager: UserDefaultsManager) {
defaults = defaultsManager
}
}
// MARK: Interceptor
extension NetworkService: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
guard let token = defaults.readAccessToken() else {
completion(.success(urlRequest))
return
}
request.setValue("Bearer \(token)", forHTTPHeaderField: RequestKeys.authorization)
completion(.success(request))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
guard request.retryCount < RetryPolicy.defaultRetryLimit else {
completion(.doNotRetry)
return
}
determineRetryAction(error, retrying: isRetrying, completion: completion)
}
private func determineRetryAction(_ error: Error, retrying: Bool, completion: @escaping (RetryResult) -> Void) {
if retrying {
completion(.retryWithDelay(2))
return
}
if error.asAFError?.responseCode == 401 && !retrying {
isRetrying = true
requestRefresh { [weak self] isSuccess in
isSuccess ? completion(.retryWithDelay(2)) : completion(.doNotRetry)
self?.isRetrying = false
}
} else {
completion(.doNotRetry)
}
}
}
// MARK: Auth
extension NetworkService {
func signInRequest(email: String, password: String,
onSuccess success: @escaping () -> Void,
onFailure failure: @escaping (_ error: Error) -> Void) {
AF.request(urlStrings.base + urlStrings.login,
method: .post,
parameters: [
RequestKeys.email: email,
RequestKeys.password: password
],
encoding: JSONEncoding.default).validate().responseData { [weak self] response in
switch response.result {
case .success(let data):
let decoder = JSONDecoder()
if let token = try? decoder.decode(TokenResponse.self, from: data) {
self?.defaults.writeTokens(accessToken: token.accessToken,
refreshToken: token.refreshToken)
success()
} else {
failure(AuthError(code: 1002))
}
case .failure(let error):
failure(AuthError(code: error.responseCode))
}
}
}
...
Класс NetworkService слишком большой и содержит в себе все запросы, которые разделены логически на extensions. Большой класс считается плохим тонном (ссылка) Тяжело читать. Лучше вынести по файлам extensions. А назвать NetworkService+Auth и тд. Это облегчит читаемость и работу с кодом.
Неиспользуемый код (мертвый код)
В целом проект выполнен с соблюдением code style. Автор молодец. Подсвечу один момент: заметил в классе AppDelegate неиспользуемый код.
Код из класса AppDelegate
@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
@available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
Почему это плохо
Код, который не используется считается плохим тоном. Его сложно читать и он может запутать программистов и коллег по цеху). Более подробно можно почитать на этом сайте
Решение
От такого кода нужно избавиться. Если нужен будет старый код, то его можно найти в истории гита. Это сделает проект более понятным и читаемым.
enum ImageNames {
// MARK: Images
static let appLogo = "AppLogo"
static let personPlaceholder = "PersonPlaceholderImage"
static let match = "MatchImage"
static let welcomePeople = "WelcomePeopleImage"
// MARK: Icons
static let streamIcon = "StreamIcon"
static let peopleIcon = "PeopleIcon"
static let chatsIcon = "ChatsIcon"
static let personIcon = "PersonIcon"
static let crossIcon = "CrossIcon"
static let heartIcon = "HeartIcon"
static let cameraIcon = "CameraIcon"
static let sendIcon = "SendIcon"
static let editIcon = "EditIcon"
}
За константы названия картинок плюс. Подсвечу два момента.
static let appLogo = UIImage(named: "AppLogo")
За константы строк в коде плюс в карму) Подсвечу следующие моменты
enum можно вкладывать в enum, это дает нам удобство использование, Вместо Strings.register
будет Strings.Headers.register
.
Отсутсвие локализации. Когда потребуется локализация, придется потратить много времени, чтобы ее добавить и перенести все строки в файлы локализации, поменять константы и тд. Советую добавлять ее сразу, даже если язык пока один. Это избавит от потенциальных проблем в будущем. (ссылка). Для удобной работы с локализацией и ресурсами(картинки) можно использовать
R.swift (ссылка) или SwiftGen (ссылка)
enum Strings {
// MARK: Button Titles
static let signUp = "Зарегистрироваться"
static let signIn = "Войти"
static let decline = "Отказ"
static let like = "Лайк"
static let edit = "Редактировать"
static let save = "Сохранить"
static let back = "Назад"
static let cancel = "Отмена"
static let writeMessage = "Написать сообщение"
static let deletePhoto = "Удалить фото"
static let choosePhoto = "Выбрать фото"
static let libraryActionTitle = "Библиотека"
static let cameraActionTitle = "Камера"
static let settingsActionTitle = "Настройки"
static let notNow = "Не сейчас"
static let ok = "Ок"
// MARK: Headers
static let authorization = "Авторизация"
static let register = "Регистрация"
static let aboutYourselfHeader = "Расскажи о себе"
static let aboutYourselfTopicsHeader = "Укажи интересы"
static let editProfileTopicsHeader = "Интересы"
static let welcomeHeader = "Ты найдешь того, кто поревьюит твой код"
static let match = "Ваши интерфейсы подошли друг к другу"
class UserDefaultsManager {
// MARK: Tokens
func readTokens() -> (accessToken: String?, refreshToken: String?) {
return (readAccessToken(), readRefreshToken())
}
func readAccessToken() -> String? {
return read(forKey: UserDefaultsKeys.accessTokenKey)
}
func readRefreshToken() -> String? {
return read(forKey: UserDefaultsKeys.refreshTokenKey)
}
func writeTokens(accessToken: String, refreshToken: String) {
write(accessToken, forKey: UserDefaultsKeys.accessTokenKey)
write(refreshToken, forKey: UserDefaultsKeys.refreshTokenKey)
}
func tokensExists() -> Bool {
let tokens = readTokens()
return tokens.accessToken != nil
&& tokens.refreshToken != nil
}
// MARK: User Id
func readUserId() -> String? {
return read(forKey: UserDefaultsKeys.userId)
}
// MARK: Common Methods
func read(forKey key: String) -> String? {
return UserDefaults.standard.string(forKey: key)
}
func write(_ value: String, forKey key: String) {
UserDefaults.standard.set(value, forKey: key)
}
func checkValue(forKey key: String) -> Bool {
return UserDefaults.standard.object(forKey: key) != nil
}
}
В проекте существует UserDefaultsManager, как property wrapper. Но в тоже время он используется только в NetworkService и SceneDelegate для работы с токенами. В других частях используется UserDefaults.standard
. Класс для токенов, а название общее. Это вводит в заблуждение разработчиков. Предлагаю использовать везде UserDefaultsManager с static методами read и write как property wrapper. А для работы с токенами создать UserDefaultsTokenManager, который будет взаимодействовать с UserDefaultsManager. И поместить эти утилиты в папку Utils (смотреть замечание про структуру проекта). Материал на эту темы. И еще
class UserMessageCell: MessageTableViewCell {
// MARK: Public Overrided Methods
override func setup() {
super.setup()
setupAvatarImageView()
setupMessageContainer()
}
// MARK: Private Setup Methods
private func setupAvatarImageView() {
avatarImageView.snp.makeConstraints { make in
make.bottom.equalTo(self).inset(Dimensions.smallInset)
make.bottom.equalTo(messageContainer.snp.bottom)
make.leading.equalTo(messageContainer.snp.trailing).inset(-Dimensions.smallInset)
make.trailing.equalTo(self).inset(Dimensions.defaultInset)
make.width.equalTo(Dimensions.mediumInset)
make.width.equalTo(avatarImageView.snp.height)
}
}
private func setupMessageContainer() {
messageContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
messageContainer.snp.makeConstraints { make in
make.top.equalTo(self)
make.leading.greaterThanOrEqualTo(self).inset(Dimensions.messageLeadingInset)
}
}
}
// MARK: Dimensions
private extension Dimensions {
static let messageLeadingInset: CGFloat = 80
}
Принято объявлять константы в начале файла. Было бы удобнее вынести константы перед основным кодом, так как до последней строчки мало кто листает. К тому же может путаться с extensions класса, которые принято объявлять после класса. При открытии файла, хочется сразу видеть все константы.
// MARK: Dimensions
private extension Dimensions {
static let messageLeadingInset: CGFloat = 80
}
class UserMessageCell: MessageTableViewCell {
// MARK: Public Overrided Methods
override func setup() {
super.setup()
setupAvatarImageView()
setupMessageContainer()
}
// MARK: Private Setup Methods
private func setupAvatarImageView() {
avatarImageView.snp.makeConstraints { make in
make.bottom.equalTo(self).inset(Dimensions.smallInset)
make.bottom.equalTo(messageContainer.snp.bottom)
make.leading.equalTo(messageContainer.snp.trailing).inset(-Dimensions.smallInset)
make.trailing.equalTo(self).inset(Dimensions.defaultInset)
make.width.equalTo(Dimensions.mediumInset)
make.width.equalTo(avatarImageView.snp.height)
}
}
private func setupMessageContainer() {
messageContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
messageContainer.snp.makeConstraints { make in
make.top.equalTo(self)
make.leading.greaterThanOrEqualTo(self).inset(Dimensions.messageLeadingInset)
}
}
}
Утечка памяти и ARC
class ChooseImageActionSheet: UIAlertController {
// MARK: Properties
var delegate: ChooseImageActionSheetDelegate?
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupActions()
}
Так же вдругих классах
delegate объявляют как weak var delegate: ChooseImageActionSheetDelegate?
ключевое слово weak делает ссылку скобой и счетчик сильных ссылок не увеличивается в механизме ARC.
Использование сильных ссылок введет к утечкам памяти, когда два класса сильно ссылаются друг на друга. Слабые ссылки позволяют нам разорвать эту связь. Материал по этой теме Swift: ARC и управление памятью
extension Date {
static func isIncreasingDateOrder(date1: Date?, date2: Date?) -> Bool {
guard let date1 = date1, let date2 = date2 else {
return date1 != nil
}
return date1 > date2
}
static func from(string: String) -> Date? {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return formatter.date(from: string.replacingOccurrences(of: "[UTC]", with: ""))
}
func toServerFormat() -> String? {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return formatter.string(from: self)
}
func toMessageFormat() -> String {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(abbreviation: TimeZone.current.identifier)
formatter.dateFormat = "HH:mm • d MMMM yyyy"
formatter.locale = Locale(identifier: "ru_RU_POSIX")
return formatter.string(from: self)
}
}
Строки как "en_US_POSIX" являются магическими строками (ссылка), их можно вынести в константы следующим образом. Завести структуру или енам для константы в коде. А в файле перед основным кодом определять константы. Это относится ко всем магическим цифрам и строкам в проекте.
private extension Constants {
static let enUSPosix = "en_US_POSIX""
}
ChooseImageActionSheetDelegate
Именовать Delegate неправильное. Правильное и согласованное именование делает код читаемым.
protocol ChooseImageActionSheetDelegate {
@available(iOS 14, *)
func PHPickerDelegate() -> PHPickerViewControllerDelegate
func imagePickerDelegate() -> (UIImagePickerControllerDelegate & UINavigationControllerDelegate)
func presentAction(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
}
func presentAction(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
должно быть func сhooseImageActionSheet(_ viewController: сhooseImageActionSheet, _ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
UserCardViewModelDelegate
protocol UserCardViewModelDelegate {
func onCardTap(user: User)
}
Аналогично func onCardTap(user: User)
-> func UserCardViewModel(_ viewModel, with user: User)
Так же поправить в других местах. Хорошея статья на эту тему
Работа с UIFont
За константы в коде плюс в карму. Подсвечу следующий момент
extension UIFont {
public enum FontType: String {
case semibold = "-SemiBold"
case regular = "-Regular"
case medium = "-Medium"
case bold = "-Bold"
}
static func montserrat(_ type: FontType = .regular, size: CGFloat = UIFont.systemFontSize) -> UIFont {
return UIFont(name: "Montserrat\(type.rawValue)", size: size)!
}
}
// MARK: Constants
extension UIFont {
static let smallerLabelMedium = UIFont.montserrat(.medium, size: 10)
static let smallLabel = UIFont.montserrat(.regular, size: 12)
static let defaultLabel = UIFont.montserrat(.regular, size: 16)
static let defaultLabelBold = UIFont.montserrat(.bold, size: 16)
static let mediumLabel = UIFont.montserrat(.regular, size: 18)
static let mediumLabelSemibold = UIFont.montserrat(.semibold, size: 18)
static let mediumLabelBold = UIFont.montserrat(.bold, size: 18)
static let largeLabelBold = UIFont.montserrat(.bold, size: 24)
static let largerLabelBold = UIFont.montserrat(.bold, size: 28)
}
Сейчас получается, что используется определенный шрифт - UIFont.montserrat
. При смене шрифта придется рефакторить код. Нужна абстракция - UIFont.appFont(.bold, size: 28)
. В appFont уже конкретный шрифт - мы можем менять реализацию без необходимости рефакторинга. Название шрифта Montserrat можно так же вынести в константы.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.