fix: AutoLayout issue. Update keyboard listener. Expose server error message

This commit is contained in:
CMK 2021-02-20 19:54:08 +08:00
parent 0e2aa4570d
commit 6285cb95fa
6 changed files with 194 additions and 42 deletions

View File

@ -83,6 +83,7 @@
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; };
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */; };
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */; };
@ -266,6 +267,7 @@
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = "<group>"; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonUser.swift"; sourceTree = "<group>"; };
@ -444,6 +446,7 @@
children = (
DB45FB0425CA87B4005A8AC7 /* APIService */,
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
);
path = Service;
sourceTree = "<group>";
@ -1247,6 +1250,7 @@
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,

View File

@ -273,6 +273,7 @@ extension AuthenticationViewController {
}
guard viewModel.isIdle.value else { return }
viewModel.isRegistering.value = true
context.apiService.instance(domain: domain)
.compactMap { [weak self] response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error>? in
guard let self = self else { return nil }
@ -289,9 +290,11 @@ extension AuthenticationViewController {
}
return authenticateInfo
}
.compactMap { [weak self] authenticateInfo -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error>? in
.compactMap { [weak self] authenticateInfo -> AnyPublisher<(Mastodon.Response.Content<Mastodon.Entity.Token>, AuthenticationViewModel.AuthenticateInfo), Error>? in
guard let self = self else { return nil }
return self.context.apiService.applicationAccessToken(domain: domain, clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret)
.map { ($0, authenticateInfo) }
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: DispatchQueue.main)
@ -305,9 +308,13 @@ extension AuthenticationViewController {
case .finished:
break
}
} receiveValue: { [weak self] response in
} receiveValue: { [weak self] response, authenticateInfo in
guard let self = self else { return }
let mastodonRegisterViewModel = MastodonRegisterViewModel(domain: domain, applicationToken: response.value)
let mastodonRegisterViewModel = MastodonRegisterViewModel(
domain: domain,
authenticateInfo: authenticateInfo,
applicationToken: response.value
)
self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show)
}
.store(in: &disposeBag)

View File

@ -20,15 +20,13 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency {
var viewModel: MastodonRegisterViewModel!
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
let stackViewTopDistance: CGFloat = 16
var keyboardFrame: CGRect!
var scrollview: UIScrollView = {
let scrollview = UIScrollView()
scrollview.showsVerticalScrollIndicator = false
scrollview.translatesAutoresizingMaskIntoConstraints = false
scrollview.keyboardDismissMode = .interactive
return scrollview
}()
@ -226,21 +224,21 @@ extension MastodonRegisterViewController {
view.addSubview(scrollview)
NSLayoutConstraint.activate([
scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
view.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor, constant: 20),
scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor),
scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
scrollview.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollview.contentLayoutGuide.widthAnchor),
])
// stackview
scrollview.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
let bottomEdgeLayoutConstraint: NSLayoutConstraint = scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: scrollview.contentLayoutGuide.topAnchor, constant: stackViewTopDistance),
stackView.leadingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.trailingAnchor),
stackView.widthAnchor.constraint(equalTo: scrollview.frameLayoutGuide.widthAnchor),
bottomEdgeLayoutConstraint,
scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
])
// photoview
@ -268,7 +266,7 @@ extension MastodonRegisterViewController {
plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor),
plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor),
])
// textfield
NSLayoutConstraint.activate([
usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh),
@ -280,34 +278,49 @@ extension MastodonRegisterViewController {
// password
stackView.setCustomSpacing(6, after: passwordTextField)
stackView.setCustomSpacing(32, after: passwordCheckLabel)
// button
signUpButton.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(signUpButton)
NSLayoutConstraint.activate([
signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
])
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
scrollview.addSubview(signUpActivityIndicatorView)
NSLayoutConstraint.activate([
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor),
])
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil)
.sink { [weak self] notification in
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
self?.keyboardFrame = endFrame
UIView.animate(withDuration: 0.3) {
bottomEdgeLayoutConstraint.constant = UIScreen.main.bounds.height - endFrame.origin.y + 26
self?.view.layoutIfNeeded()
}
Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] isShow, state, endFrame in
guard let self = self else { return }
guard isShow, state == .dock else {
self.scrollview.contentInset.bottom = 0.0
self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0
return
}
.store(in: &disposeBag)
// isShow AND dock state
let contentFrame = self.view.convert(self.scrollview.frame, to: nil)
let padding = contentFrame.maxY - endFrame.minY
guard padding > 0 else {
self.scrollview.contentInset.bottom = 0.0
self.scrollview.verticalScrollIndicatorInsets.bottom = 0.0
return
}
self.scrollview.contentInset.bottom = padding + 16
self.scrollview.verticalScrollIndicatorInsets.bottom = padding + 16
})
.store(in: &disposeBag)
viewModel.isRegistering
.receive(on: DispatchQueue.main)
.sink { [weak self] isRegistering in
@ -317,7 +330,7 @@ extension MastodonRegisterViewController {
self.signUpButton.isEnabled = !isRegistering
}
.store(in: &disposeBag)
viewModel.isUsernameValid
.receive(on: DispatchQueue.main)
.sink { [weak self] isValid in
@ -359,7 +372,7 @@ extension MastodonRegisterViewController {
self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false
}
.store(in: &disposeBag)
viewModel.error
.compactMap { $0 }
.receive(on: DispatchQueue.main)
@ -375,6 +388,7 @@ extension MastodonRegisterViewController {
)
}
.store(in: &disposeBag)
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: passwordTextField)
.receive(on: DispatchQueue.main)
@ -385,20 +399,26 @@ extension MastodonRegisterViewController {
self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2)
}
.store(in: &disposeBag)
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
}
}
extension MastodonRegisterViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
var bottomOffsetY: CGFloat = textField.frame.origin.y + textField.frame.height - scrollview.frame.height + keyboardFrame.size.height + stackViewTopDistance
if textField == passwordTextField {
bottomOffsetY += passwordCheckLabel.frame.height
}
if bottomOffsetY > 0 {
scrollview.setContentOffset(CGPoint(x: 0, y: bottomOffsetY), animated: true)
// align to password label when overlap
if textField === passwordTextField,
KeyboardResponderService.shared.isShow.value,
KeyboardResponderService.shared.state.value == .dock {
let endFrame = KeyboardResponderService.shared.endFrame.value
let contentFrame = self.scrollview.convert(self.passwordCheckLabel.frame, to: nil)
let padding = contentFrame.maxY - endFrame.minY
if padding > 0 {
let contentOffsetY = scrollview.contentOffset.y
DispatchQueue.main.async {
self.scrollview.setContentOffset(CGPoint(x: 0, y: contentOffsetY + padding + 16.0), animated: true)
}
}
}
}
@ -454,7 +474,10 @@ extension MastodonRegisterViewController {
@objc private func signUpButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
if !validateAllTextField() {
guard validateAllTextField(),
let username = viewModel.username.value,
let email = viewModel.email.value,
let password = viewModel.password.value else {
return
}
@ -463,9 +486,9 @@ extension MastodonRegisterViewController {
let query = Mastodon.API.Account.RegisterQuery(
reason: nil,
username: viewModel.username.value!,
email: viewModel.email.value!,
password: viewModel.password.value!,
username: username,
email: email,
password: password,
agreement: true, // TODO:
locale: "en" // TODO:
)

View File

@ -15,7 +15,9 @@ final class MastodonRegisterViewModel {
// input
let domain: String
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
let applicationToken: Mastodon.Entity.Token
let isRegistering = CurrentValueSubject<Bool, Never>(false)
let username = CurrentValueSubject<String?, Never>(nil)
let displayname = CurrentValueSubject<String?, Never>(nil)
@ -32,8 +34,13 @@ final class MastodonRegisterViewModel {
let error = CurrentValueSubject<Error?, Never>(nil)
init(domain: String, applicationToken: Mastodon.Entity.Token) {
init(
domain: String,
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
applicationToken: Mastodon.Entity.Token
) {
self.domain = domain
self.authenticateInfo = authenticateInfo
self.applicationToken = applicationToken
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)

View File

@ -0,0 +1,87 @@
//
// KeyboardResponderService.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-2-20.
//
import UIKit
import Combine
final class KeyboardResponderService {
var disposeBag = Set<AnyCancellable>()
// MARK: - Singleton
public static let shared = KeyboardResponderService()
// output
let isShow = CurrentValueSubject<Bool, Never>(false)
let state = CurrentValueSubject<KeyboardState, Never>(.none)
let endFrame = CurrentValueSubject<CGRect, Never>(.zero)
private init() {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil)
.sink { notification in
self.isShow.value = true
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification, object: nil)
.sink { notification in
self.isShow.value = false
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil)
.sink { notification in
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
}
}
extension KeyboardResponderService {
private func updateInternalStatus(notification: Notification) {
guard let isLocal = notification.userInfo?[UIWindow.keyboardIsLocalUserInfoKey] as? Bool,
let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
self.endFrame.value = endFrame
guard isLocal else {
self.state.value = .notLocal
return
}
// check if floating
guard endFrame.width == UIScreen.main.bounds.width else {
self.state.value = .floating
return
}
// check if undock | split
let dockMinY = UIScreen.main.bounds.height - endFrame.height
if endFrame.minY < dockMinY {
self.state.value = .notDock
} else {
self.state.value = .dock
}
}
}
extension KeyboardResponderService {
enum KeyboardState {
case none
case notLocal
case notDock // undock | split
case floating // iPhone size floating
case dock
}
}

View File

@ -34,3 +34,27 @@ extension Mastodon.API {
}
}
extension Mastodon.API.Error: LocalizedError {
public var errorDescription: String? {
guard let mastodonError = mastodonError else {
return nil
}
switch mastodonError {
case .generic(let error):
return error.error
}
}
public var failureReason: String? {
guard let mastodonError = mastodonError else {
return nil
}
switch mastodonError {
case .generic(let error):
return error.errorDescription
}
}
}