forked from zelo72/mastodon-ios
fix: AutoLayout issue. Update keyboard listener. Expose server error message
This commit is contained in:
parent
0e2aa4570d
commit
6285cb95fa
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue