タグ4.1のコードは、この記事がデモと一貫していることを確認するために引っ張ることができます。
ログイン登録UIの実装
プロジェクトが起動した後に最初に表示されるのは、通常のログインと登録プロセスです。これは基本的に本番環境でそのまま使用できますが、デモで使用するインターフェースデータは静的なものです。
これらのページは Scene/Login/LoginVCs.swift あり、5つのページが200行強のビジネスコードで、コードのほとんどがインターフェースを要求していることがわかります。
フォームがバリデートされていること、入力が送信ボタンの条件を満たしていないこと、送信ボタンがクリックできないこと、プロンプトが表示されること、入力ボックスが次のフィールドに切り替え可能であることにお気づきかもしれません:
では、フォームのバリデーションとジャンプはどこに実装されているのでしょうか?Login.storyboardを開いて、各フォームには Form Field Verify Control オブジェクトがあり、 フォームの 入力ボックスと送信ボタンをつないでいることを見てみましょう:
この関連付けの後、入力ボックスのテキストが変更されると、コントロールは関連付けられた入力ボックスにバリデーションされているかどうかを尋ねます。
現在の入力ボックスが何を入力すべきかを定義するために定義された文字列列列挙のセットから始まります:
enum TextFieldContentType: String {
case mobile //
case code //
case password // パスワードは、長さを検証せず、空でないことだけを検証する。
case password2 // パスワード認証、厳格な認証
case userName //
case email // 電子メール
case name //
case required // 空でない場合は、プレースホルダー・ヒントを使用する。
}
その後、異なるコンテンツに基づいてバリデーションが行われ、バリデーションメソッドは次のように定義されます:
class TextField: MBTextField {
/// 自動的にバリデーションを行い、プロンプトを表示し、正当な値を返す
///
/// - Parameters:
/// - noticeWhenInvaild: 内容が不正な場合にエラーメッセージをポップアップする
/// - becomeFirstResponderWhenInvaild: コンテンツが不正なときにキーボード・フォーカスを得る
/// - Returns:
func vaildFieldText(noticeWhenInvaild: Bool = true, becomeFirstResponderWhenInvaild: Bool = true) -> String? {
...
}
}
入力ボックス間の切り替えは、nextFieldプロパティを介して関連付けられており、入力ボックスのnextFieldが別の入力ボックスを指している場合は、Enterキーで次の入力ボックスにフォーカスを切り替えます。
実装の詳細は General/Text/TextField.swift および MBAppKit/MBTextFieldを参照してください。
また、フォームのUIが別のUITableViewControllerで定義されていることにお気づきかもしれません。これはScene/Login/LoginForms.swiftコードで説明されています。
パスワードを変更するには2つのステップがあります。1つ目のステップは、CAPTCHAを通して本人確認を行い、ワンタイムトークンを取得することです。セグエジャンプでトークンを渡すデフォルトの方法は、フレームワーク入門 - メソッドを渡すインターフェイスを参照してください。
ログイン開始
ログインに成功した後のインターフェイスの応答は非常に単純で、ユーザー情報、トークンおよびその他の情報を取得し、ユーザーオブジェクトを作成し、最後にAccount.currentが完了すると、関連するコードを設定します:
// LoginVCs.swift Line: 65
// ログインリクエスト
API.requestName("SignInUp") { c in
c.parameters = ["mobile": mobile, "code": code]
...
c.success { _, rsp in
guard let item = rsp as? LoginResponseEntity else { fatalError() }
item.setAsCurrent()
}
}
// LoginResponseEntity.swift Line: 41
/// サーバーのログイン情報を受け取り、現在のユーザーを設定する
func setAsCurrent() {
guard let info = info, let token = token else {
AppHUD().showErrorStatus("サーバーのリターン情報がない")
return
}
let user = Account(id: info.uid as String)
user?.token = token
Account.current = user
}
インターフェイスのジャンプ、メッセージの保存、複数のサービスの設定などです。その場合、ユーザーモジュールには2つのメソッドがあるはずです: ログイン時に行うものと、ログアウト時にその反対のことを行うものです。しかし、vcでもuserモジュールでも、他のモジュールから多くのコードが直接導入されています。これは、プロジェクトが複雑になるにつれて問題になる可能性があります:
- モジュールのコードの一部は外部に書かれており、ビジネスが複雑な場合、多くの変更はこの外部に書かれたコードの部分を見逃しやすく、その結果バグが発生します;
- モジュールの寿命にはサイクルの問題があります。ログインメソッドで他のモジュールを参照すると、機能がすぐに必要でなくても、必然的に作成する必要があります。実際に発生する別のバグは、アプリケーションの起動後にユーザーモジュールが初期化され、ログインステータスを決定し、他のモジュールをセットアップしますが、その後、ユーザーモジュールが他のモジュールの1つに導入され、行き止まりのユーザーモジュール作成ループが発生します。そのうちのいくつかは、セットアップを遅らせることで回避できますがすぐに設定する必要があるものは避けてください。
また、モジュールの参照に関する原則(基本モジュールはより上位のモジュールを参照すべきではない)など、モジュールに関するいくつかの関連問題もあります。
ここで提供されるメカニズムは、外部モジュールが Account にアカウント変更イベントを登録し、Account.current が変更されたときに、それらの外部モジュールに通知できるというものです。例えば、navigation:
class NavigationController: MBNavigationController {
override func viewDidLoad() {
super.viewDidLoad()
...
// initial trueの場合、呼び出されたときに実行される closure
Account.addCurrentUserChangeObserver(self, initial: true) { [weak self] user in
if user != nil {
// ホームページにジャンプする
self?.onLogin()
} else {
// ユーザーログインページにジャンプする
self?.onLogout()
}
}
}
}
ナビゲーター
ログイン後、アプリケーションのメインインターフェイスに入り、一番下にタブバーがありますが、このタブはUITabBarControllerによって実装されているのではなく、NavigationControllerによって保持・管理されているカスタムビューです:
その可視性はナビゲーションによって制御され、vcは属性を介してタブを表示する必要があるかどうかを宣言することができます。フレームワーク入門 - ユニークナビゲーションも参照してください。
ポストセクション
リストレイアウト
デモのメインページは TopicListDisplayer タイプとダークモードをサポートしています。 リストセルのレイアウトはちょっとしたトリックを使用しており、イメージとテキストは2つのカラムとして見ることができ、セルの高さは2つのうち最も高いものに基づいています。レイアウトは主にスタックビューに依存しています。
リスト コード 組織
投稿リストはUITableViewControllerでもある TopicListDisplayer定義され、UI再利用のために特定のリストページを埋め込んで表示します。
詳細レイアウト
詳細記事のコンテンツ部分は MBTableHeaderFooterViewを 使用して高さを適応させ、下部のボタンエリアは MBBottomLayoutViewを 使用してiPhone Xやその他の不規則な長方形のスクリーンを持つデバイスをサポートし、自動的に位置を調整して角を丸く切り取ります:
モデル更新後のリフレッシュ
ポストモデルにおける「好き」の切り替え方法は、以下のように簡略化されています:
class TopicEntity: MBModel {
...
@objc private(set) var isLiked: Bool = false
private weak var likeTask: RFAPITask?
/// 状態を切り替える
func toggleLike() {
let positiveAPI = "TopicLikedAdd"
let negativeAPI = "TopicLikedRemove"
if let task = likeTask {
task.cancel()
likeTask = nil
return
}
let shouldLike = !isLiked
isLiked = shouldLike
delegates.invoke { $0.topicLikedChanged?(self) }
likeTask = API.requestName(shouldLike ? positiveAPI : negativeAPI, context: { c in
c.parameters = ["tid": self.uid]
c.complation { task, _, _ in
if task?.isSuccess == false {
self.isLiked = !shouldLike
self.delegates.invoke { $0.topicLikedChanged?(self) }
}
}
})
}
lazy var delegates = MulticastDelegate<TopicEntityUpdating>()
}
// 状態更新プロトコル
// オプションの実装が必要である。 @objc
@objc protocol TopicEntityUpdating {
@objc optional func topicLikedChanged(_ item: TopicEntity)
...
}
あなたがリスナーを追加するには、モデルの変更に注意を払う必要がある、手動で削除する必要はありませんが、オブジェクトのリリースは自動的にリンクが解除されますが、リストセルが再利用され、削除する必要があります。ポストリストセルの実装は、次のように簡素化されます:
/// ポストリスト cell
class TopicListCell: UITableViewCell, TopicEntityUpdating {
@objc var item: TopicEntity! {
didSet {
if let old = oldValue {
old.delegates.remove(self)
}
item.delegates.add(self)
... // その他のUI設定
topicLikedChanged(item)
}
}
@IBOutlet private weak var likeButton: UIButton!
@IBAction private func onLikeButtonTapped(_ sender: Any) {
item.toggleLike()
}
func topicLikedChanged(_ item: TopicEntity) {
likeButton.isSelected = item.isLiked
likeButton.text = (item.isLiked ? " " : "") + " \(item.likeCount)"
}
}




