diff --git a/Package.swift b/Package.swift index 319473a..d1ada7d 100644 --- a/Package.swift +++ b/Package.swift @@ -15,26 +15,11 @@ let package = Package( ) ], dependencies: [ - .package( - name: "SPAlert", - url: "https://github.com/ivanvorobei/SPAlert", .upToNextMajor(from: "4.2.0") - ), - .package( - name: "NativeUIKit", - url: "https://github.com/ivanvorobei/NativeUIKit", .upToNextMajor(from: "1.3.7") - ), - .package( - name: "SPFirebase", - url: "https://github.com/ivanvorobei/SPFirebase", .upToNextMajor(from: "1.0.6") - ), - .package( - name: "SPSafeSymbols", - url: "https://github.com/sparrowcode/SPSafeSymbols", .upToNextMajor(from: "1.0.5") - ), - .package( - name: "Nuke", - url: "https://github.com/kean/Nuke", .upToNextMajor(from: "10.7.1") - ) + .package(url: "https://github.com/ivanvorobei/SPAlert", .upToNextMajor(from: "4.2.0")), + .package(url: "https://github.com/ivanvorobei/NativeUIKit", .upToNextMajor(from: "1.3.7")), + .package(url: "https://github.com/ivanvorobei/SPFirebase", .upToNextMajor(from: "1.0.6")), + .package(url: "https://github.com/sparrowcode/SPSafeSymbols", .upToNextMajor(from: "1.0.5")), + .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "10.7.1")) ], targets: [ .target( diff --git a/Sources/SPProfiling/Data/Texts.swift b/Sources/SPProfiling/Data/Texts.swift index 7d91806..76213b9 100644 --- a/Sources/SPProfiling/Data/Texts.swift +++ b/Sources/SPProfiling/Data/Texts.swift @@ -26,6 +26,8 @@ enum Texts { public static var save: String { NSLocalizedString("save", bundle: .module, comment: "") } public static var cancel: String { NSLocalizedString("cancel", bundle: .module, comment: "") } + // MARK: - Error + enum Error { static var not_found: String { NSLocalizedString("error not found", bundle: .module, comment: "") } @@ -39,29 +41,32 @@ enum Texts { static var cant_present: String { NSLocalizedString("error auth cant present", bundle: .module, comment: "") } static var cant_prepare_requerid_data: String { NSLocalizedString("error auth cant prepare requerid data", bundle: .module, comment: "") } } - } - - enum Profile { - - static var name_didnt_set: String { NSLocalizedString("profile name didint set", bundle: .module, comment: "") } - enum Error { + enum Profile { - static var name_short: String { NSLocalizedString("profile error name short", bundle: .module, comment: "") } - static var name_long: String { NSLocalizedString("profile error name long", bundle: .module, comment: "") } - static var empty_name: String { NSLocalizedString("profile error empty name", bundle: .module, comment: "") } - static var big_avatar_width: String { NSLocalizedString("profile error big avatar width", bundle: .module, comment: "") } - static var big_avatar_size: String { NSLocalizedString("profile error big avatar size", bundle: .module, comment: "") } + static var name_short: String { NSLocalizedString("error profile name short", bundle: .module, comment: "") } + static var name_long: String { NSLocalizedString("error profile name long", bundle: .module, comment: "") } + static var empty_name: String { NSLocalizedString("error profile empty name", bundle: .module, comment: "") } + static var big_avatar_width: String { NSLocalizedString("error profile big avatar width", bundle: .module, comment: "") } + static var big_avatar_size: String { NSLocalizedString("error profile big avatar size", bundle: .module, comment: "") } } + } + + // MARK: - Auth + + enum Auth { - enum Auth { - - static var continue_anonymously: String { NSLocalizedString("profile auth continue anonymously", bundle: .module, comment: "") } - - static var title: String { NSLocalizedString("profile auth title", bundle: .module, comment: "") } - static var description: String { NSLocalizedString("profile auth description", bundle: .module, comment: "") } - static var footer_description: String { NSLocalizedString("profile auth footer description", bundle: .module, comment: "") } - } + static var sign_in: String { NSLocalizedString("auth sign in", bundle: .module, comment: "") } + static var continue_anonymously: String { NSLocalizedString("auth continue anonymously", bundle: .module, comment: "") } + static var description: String { NSLocalizedString("auth description", bundle: .module, comment: "") } + static var footer_description: String { NSLocalizedString("auth footer description", bundle: .module, comment: "") } + } + + // MARK: - Profile + + enum Profile { + + static var placeholder_name: String { NSLocalizedString("profile placeholder name", bundle: .module, comment: "") } enum Devices { diff --git a/Sources/SPProfiling/Interface/Profile/CurrentProfileController+Actions.swift b/Sources/SPProfiling/Interface/Profile/ProfileController+Actions.swift similarity index 98% rename from Sources/SPProfiling/Interface/Profile/CurrentProfileController+Actions.swift rename to Sources/SPProfiling/Interface/Profile/ProfileController+Actions.swift index a1138e7..064789b 100644 --- a/Sources/SPProfiling/Interface/Profile/CurrentProfileController+Actions.swift +++ b/Sources/SPProfiling/Interface/Profile/ProfileController+Actions.swift @@ -19,14 +19,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import UIKit import SparrowKit import NativeUIKit import SPDiffable import SPAlert -extension CurrentProfileController { +extension ProfileController { internal func showTextFieldToChangeName() { let alertController = UIAlertController(title: Texts.Profile.Actions.Rename.alert_title, message: Texts.Profile.Actions.Rename.alert_description, preferredStyle: .alert) diff --git a/Sources/SPProfiling/Interface/Profile/CurrentProfileController+Internal.swift b/Sources/SPProfiling/Interface/Profile/ProfileController+Internal.swift similarity index 97% rename from Sources/SPProfiling/Interface/Profile/CurrentProfileController+Internal.swift rename to Sources/SPProfiling/Interface/Profile/ProfileController+Internal.swift index 4b0934d..36564bb 100644 --- a/Sources/SPProfiling/Interface/Profile/CurrentProfileController+Internal.swift +++ b/Sources/SPProfiling/Interface/Profile/ProfileController+Internal.swift @@ -26,7 +26,7 @@ import SPDiffable import SPSafeSymbols import SPAlert -extension CurrentProfileController { +extension ProfileController { internal func configureHeader() { setProfile(profileModel, completion: { [weak self] in @@ -99,7 +99,7 @@ extension CurrentProfileController { internal func setProfile(_ profileModel: ProfileModel, completion: (()->())? = nil) { headerView.nameLabel.text = profileModel.name - headerView.namePlaceholderLabel.text = Texts.Profile.name_didnt_set + headerView.namePlaceholderLabel.text = Texts.Profile.placeholder_name if let email = profileModel.email { headerView.emailButton.setTitle(email) } headerView.layoutSubviews() diff --git a/Sources/SPProfiling/Interface/Profile/CurrentProfileController.swift b/Sources/SPProfiling/Interface/Profile/ProfileController.swift similarity index 99% rename from Sources/SPProfiling/Interface/Profile/CurrentProfileController.swift rename to Sources/SPProfiling/Interface/Profile/ProfileController.swift index 6f4daad..3a56093 100644 --- a/Sources/SPProfiling/Interface/Profile/CurrentProfileController.swift +++ b/Sources/SPProfiling/Interface/Profile/ProfileController.swift @@ -19,7 +19,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import UIKit import SparrowKit import NativeUIKit @@ -27,7 +26,7 @@ import SPDiffable import SPSafeSymbols import SPAlert -class CurrentProfileController: NativeProfileController { +class ProfileController: NativeProfileController { internal var profileModel: ProfileModel diff --git a/Sources/SPProfiling/Interface/Profile/Table/CellProvider+Profile.swift b/Sources/SPProfiling/Interface/Profile/Table/CellProvider+Profile.swift new file mode 100644 index 0000000..0be35d7 --- /dev/null +++ b/Sources/SPProfiling/Interface/Profile/Table/CellProvider+Profile.swift @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.by) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit +import SPDiffable + +extension SPDiffableTableDataSource.CellProvider { + + public static var profile: SPDiffableTableDataSource.CellProvider { + return SPDiffableTableDataSource.CellProvider() { (tableView, indexPath, item) -> UITableViewCell? in + guard let _ = item as? DiffableProfileItem else { return nil } + let cell = tableView.dequeueReusableCell(withClass: ProfileTableViewCell.self, for: indexPath) + return cell + } + } +} diff --git a/Sources/SPProfiling/Interface/Profile/Table/DiffableProfileItem.swift b/Sources/SPProfiling/Interface/Profile/Table/DiffableProfileItem.swift new file mode 100644 index 0000000..c970c56 --- /dev/null +++ b/Sources/SPProfiling/Interface/Profile/Table/DiffableProfileItem.swift @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.by) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit +import SPDiffable + +open class DiffableProfileItem: SPDiffableActionableItem { + + public static let id: String = "spprofiling-profile-item" + + public init(action: SPDiffableActionableItem.Action? = nil) { + super.init(id: Self.id, action: action) + } +} diff --git a/Sources/SPProfiling/Interface/Profile/Table/ProfileTableViewCell.swift b/Sources/SPProfiling/Interface/Profile/Table/ProfileTableViewCell.swift new file mode 100644 index 0000000..cfc9b90 --- /dev/null +++ b/Sources/SPProfiling/Interface/Profile/Table/ProfileTableViewCell.swift @@ -0,0 +1,146 @@ +// The MIT License (MIT) +// Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.by) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit +import NativeUIKit +import SparrowKit + +open class ProfileTableViewCell: SPTableViewCell { + + public let titleLabel = SPLabel().do { + $0.numberOfLines = 1 + $0.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold) + $0.textColor = .label + } + + public let descriptionLabel = SPLabel().do { + $0.numberOfLines = 1 + $0.font = UIFont.preferredFont(forTextStyle: .footnote, weight: .regular) + $0.textColor = .secondaryLabel + } + + public let avatarView = NativeAvatarView().do { + $0.isEditable = false + $0.placeholderImage = UIImage.system("person.crop.circle.fill", font: .systemFont(ofSize: 52, weight: .medium)) + $0.avatarAppearance = .placeholder + } + + // MARK: - Init + + open override func commonInit() { + super.commonInit() + higlightStyle = .content + contentView.addSubviews(avatarView, titleLabel, descriptionLabel) + accessoryType = .disclosureIndicator + updateAppearance() + } + + open override func prepareForReuse() { + super.prepareForReuse() + configureObservers() + updateAppearance() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Ovveride + + open override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + let higlightContent = (higlightStyle == .content) + if higlightContent { + [avatarView, titleLabel, descriptionLabel].forEach({ $0?.alpha = highlighted ? 0.6 : 1 }) + } + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + avatarView.sizeToFit() + avatarView.setXToSuperviewLeftMargin() + avatarView.frame.origin.y = contentView.layoutMargins.top + + let avatarRightSpace: CGFloat = NativeLayout.Spaces.default + let labelVerticalSpace: CGFloat = NativeLayout.Spaces.step / 2 + let labelWidth = contentView.layoutWidth - avatarView.frame.width - avatarRightSpace + titleLabel.layoutDynamicHeight(width: labelWidth) + descriptionLabel.layoutDynamicHeight(width: labelWidth) + + titleLabel.frame.origin.x = avatarView.frame.maxX + avatarRightSpace + descriptionLabel.frame.origin.x = titleLabel.frame.origin.x + + let labelHeight = titleLabel.frame.height + labelVerticalSpace + descriptionLabel.frame.height + if (avatarView.frame.origin.y + labelHeight) > avatarView.frame.maxY { + titleLabel.frame.origin.y = contentView.layoutMargins.top + } else { + titleLabel.frame.origin.y = contentView.layoutMargins.top + (contentView.layoutHeight - labelHeight) / 2 + } + + descriptionLabel.frame.origin.y = titleLabel.frame.maxY + labelVerticalSpace + } + + open override func sizeThatFits(_ size: CGSize) -> CGSize { + layoutSubviews() + return .init(width: size.width, height: max(avatarView.frame.maxY, descriptionLabel.frame.maxY) + contentView.layoutMargins.bottom) + } + + // MARK: - Internal + + internal func configureObservers() { + // Clean + NotificationCenter.default.removeObserver(self) + + // Configure New + NotificationCenter.default.addObserver(forName: SPProfiling.didChangedAuthState, object: nil, queue: nil) { [weak self] _ in + guard let self = self else { return } + self.updateAppearance() + } + + NotificationCenter.default.addObserver(forName: SPProfiling.didReloadedProfile, object: nil, queue: nil) { [weak self] _ in + guard let self = self else { return } + self.updateAppearance() + } + } + + internal func updateAppearance() { + if ProfileModel.isAuthed, let profileModel = ProfileModel.currentProfile { + setProfile(profileModel, completion: nil) + } else { + setAuthAppearance() + } + } + + internal func setAuthAppearance() { + avatarView.avatarAppearance = .placeholder + titleLabel.text = Texts.Auth.sign_in + titleLabel.textColor = .tint + } + + internal func setProfile(_ profileModel: ProfileModel, completion: (()->())? = nil) { + titleLabel.text = profileModel.name ?? profileModel.email ?? Texts.Profile.placeholder_name + avatarView.setAvatar(of: profileModel) { + completion?() + } + } +} diff --git a/Sources/SPProfiling/Models/Middleware/ProfileMiddlewareError.swift b/Sources/SPProfiling/Models/Middleware/ProfileMiddlewareError.swift index 7187b0b..7bbaef2 100644 --- a/Sources/SPProfiling/Models/Middleware/ProfileMiddlewareError.swift +++ b/Sources/SPProfiling/Models/Middleware/ProfileMiddlewareError.swift @@ -32,12 +32,12 @@ public enum ProfileMiddlewareError: LocalizedError { public var errorDescription: String? { switch self { - case .nameShort: return Texts.Profile.Error.name_short - case .nameLong: return Texts.Profile.Error.name_long - case .emptyName: return Texts.Profile.Error.empty_name - case .invalidEmail: return Texts.Profile.Error.empty_name - case .avatarBigWidth: return Texts.Profile.Error.big_avatar_width - case .avatarBigSize: return Texts.Profile.Error.big_avatar_size + case .nameShort: return Texts.Error.Profile.name_short + case .nameLong: return Texts.Error.Profile.name_long + case .emptyName: return Texts.Error.Profile.empty_name + case .invalidEmail: return Texts.Error.Profile.empty_name + case .avatarBigWidth: return Texts.Error.Profile.big_avatar_width + case .avatarBigSize: return Texts.Error.Profile.big_avatar_size } } } diff --git a/Sources/SPProfiling/ProfileModelExtension.swift b/Sources/SPProfiling/ProfileModelExtension.swift index 4c8d963..6906b53 100644 --- a/Sources/SPProfiling/ProfileModelExtension.swift +++ b/Sources/SPProfiling/ProfileModelExtension.swift @@ -80,7 +80,7 @@ extension ProfileModel { public static func showCurrentProfile(on viewController: UIViewController) { guard currentProfile != nil else { return } - let controller = CurrentProfileController() + let controller = ProfileController() let navigationController = controller.wrapToNavigationController(prefersLargeTitles: false) controller.navigationItem.rightBarButtonItem = controller.closeBarButtonItem