iOS学习

189 阅读5分钟

VIPER(View, Interactor, Presenter, Entity, Router)是一种在Swift的iOS开发中使用的设计模式。它是一种实现清晰的分层架构的方法,每个组件都有明确的职责,这使得应用程序容易测试和维护。

以下是一个简单的登录功能的VIPER架构示例:

首先,定义各个组件的协议:

// View
protocol LoginViewProtocol: AnyObject {
    func showAlert(message: String)
}

// Interactor
protocol LoginInteractorProtocol: AnyObject {
    func login(with username: String, password: String)
}

// Presenter
protocol LoginPresenterProtocol: AnyObject {
    var view: LoginViewProtocol? { get set }
    var interactor: LoginInteractorProtocol? { get set }
    
    func authenticateUser(with username: String, password: String)
}

// Router
protocol LoginRouterProtocol: AnyObject {
    static func createLoginModule() -> UIViewController
}

然后,我们实现这些协议:

class LoginViewController: LoginViewProtocol {
    var presenter: LoginPresenterProtocol?
    
    @IBAction func loginButtonPressed() {
        let username = "example_username"
        let password = "example_password"
        
        self.presenter?.authenticateUser(with: username, password: password)
    }
    
    func showAlert(message: String) {
        // Show alert here
    }
}

class LoginInteractor: LoginInteractorProtocol {
    func login(with username: String, password: String) {
        // Do your API call or database operations here
    }
}

class LoginPresenter: LoginPresenterProtocol {
    weak var view: LoginViewProtocol?
    var interactor: LoginInteractorProtocol?
    
    func authenticateUser(with username: String, password: String) {
        interactor?.login(with: username, password: password)
    }
}

class LoginRouter: LoginRouterProtocol {
    static func createLoginModule() -> UIViewController {
        let view = LoginViewController()
        let presenter = LoginPresenter()
        let interactor = LoginInteractor()

        presenter.view = view
        presenter.interactor = interactor
        view.presenter = presenter

        return view
    }
}

这样,我们就创建了一个基于VIPER模式的登录模块。用户交互事件被ViewController传递给PresenterPresenter调用Interactor来完成核心业务逻辑,并更新ViewRouter则负责处理视图跳转逻辑。各个组件之间的耦合度降低,可以独立进行单元测试,代码也更容易理解和维护。

注意:VIPER模式较为复杂,更适合大型项目或需要高度模块化的项目。对于小型或简单的项目,MVC(Model-View-Controller)或MVVM(Model-View-ViewModel)可能是更好的选择。

在扩展VIPER架构的登录功能时,我们需要详细定义界面(UI)和逻辑。首先,让我们向LoginViewProtocol添加更多方法来处理UI:

// View
protocol LoginViewProtocol: AnyObject {
    func showLoading()
    func hideLoading()
    func showAlert(message: String)
    func authenticationSuccessful()
}

然后,我们在LoginViewController中实现这些方法:

class LoginViewController: UIViewController, LoginViewProtocol {
    var presenter: LoginPresenterProtocol?
    
    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
    
    @IBAction func loginButtonPressed() {
        guard let username = usernameTextField.text,
              let password = passwordTextField.text else {
            return
        }
        
        self.presenter?.authenticateUser(with: username, password: password)
    }

    func showLoading() {
        loadingIndicator.startAnimating()
    }

    func hideLoading() {
        loadingIndicator.stopAnimating()
    }

    func showAlert(message: String) {
        let alert = UIAlertController(title: "Alert", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }

    func authenticationSuccessful() {
        // Perform segue or present another view controller
    }
}

接着,我们需要更新LoginInteractorProtocol以及LoginPresenterProtocol,向它们添加回调,以便在登录完成后通知ViewController

// Interactor
protocol LoginInteractorProtocol: AnyObject {
    func login(with username: String, password: String, completion: @escaping (Result<Void, Error>) -> Void)
}

// Presenter
protocol LoginPresenterProtocol: AnyObject {
    var view: LoginViewProtocol? { get set }
    var interactor: LoginInteractorProtocol? { get set }

    func authenticateUser(with username: String, password: String)
}

最后,我们在LoginInteractorLoginPresenter中实现新的方法:

class LoginInteractor: LoginInteractorProtocol {
    func login(with username: String, password: String, completion: @escaping (Result<Void, Error>) -> Void) {
        // Simulate a network call
        DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
            if username == "example_username" && password == "example_password" {
                completion(.success(()))
            } else {
                completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid username or password"])))
            }
        }
    }
}

class LoginPresenter: LoginPresenterProtocol {
    weak var view: LoginViewProtocol?
    var interactor: LoginInteractorProtocol?

    func authenticateUser(with username: String, password: String) {
        view?.showLoading()
        interactor?.login(with: username, password: password) { [weak self] result in
            DispatchQueue.main.async {
                self?.view?.hideLoading()
                switch result {
                case .success:
                    self?.view?.authenticationSuccessful()
                case .failure(let error):
                    self?.view?.showAlert(message: error.localizedDescription)
                }
            }
        }
    }
}

现在,我们的VIPER架构的登录功能已经有了更明确的用户界面和业务逻辑。当用户点击登录按钮时,应用程序将显示一个加载指示器,并开始模拟网络调用。如果用户名和密码正确,将会通知用户认证成功并导航到另一屏

在上述的VIPER架构示例中,我们创建了一个基本的登录模块。现在,我们将它进行拓展,增添一些用户界面和逻辑。

首先,我们更新LoginViewProtocol,添加显示加载状态和清除输入框的功能:

protocol LoginViewProtocol: AnyObject {
    func showAlert(message: String)
    func showLoading()
    func hideLoading()
    func clearFields()
}

然后,在LoginViewController中实现这些新方法:

class LoginViewController: LoginViewProtocol {
    // ...其他代码...

    @IBOutlet weak var usernameField: UITextField!
    @IBOutlet weak var passwordField: UITextField!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    @IBAction func loginButtonPressed() {
        // 显示加载状态
        showLoading()

        let username = usernameField.text ?? ""
        let password = passwordField.text ?? ""

        presenter?.authenticateUser(with: username, password: password)
    }

    func showAlert(message: String) {
        // ...显示警告的代码...
        
        // 错误发生时,清除输入框并隐藏加载状态
        clearFields()
        hideLoading()
    }

    func showLoading() {
        activityIndicator.startAnimating()
    }

    func hideLoading() {
        activityIndicator.stopAnimating()
    }

    func clearFields() {
        usernameField.text = ""
        passwordField.text = ""
    }
}

接下来,更新LoginInteractorProtocol,使其可以通知Presenter登录结果:

protocol LoginInteractorProtocol: AnyObject {
    var presenter: LoginPresenterProtocol? { get set }

    func login(with username: String, password: String)
}

然后,我们在LoginInteractor中模拟登录过程,并通知Presenter结果:

class LoginInteractor: LoginInteractorProtocol {
    weak var presenter: LoginPresenterProtocol?

    func login(with username: String, password: String) {
        // 模拟网络请求
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            // 假设只有当用户名和密码都是"correct"时,登录成功
            if username == "correct" && password == "correct" {
                self.presenter?.didAuthenticateUser(success: true)
            } else {
                self.presenter?.didAuthenticateUser(success: false)
            }
        }
    }
}

同时,我们需要更新LoginPresenterProtocol,使其能够处理登录结果:

protocol LoginPresenterProtocol: AnyObject {
    var view: LoginViewProtocol? { get set }
    var interactor: LoginInteractorProtocol? { get set }
    
    func authenticateUser(with username: String, password: String)
    func didAuthenticateUser(success: Bool)
}

然后,在LoginPresenter中处理登录结果,并更新View的状态:

class LoginPresenter: LoginPresenterProtocol {
    weak var view: LoginViewProtocol?
    var interactor: LoginInteractorProtocol? {
        didSet {
            interactor?.presenter = self
        }
    }

    func authenticateUser(with username: String, password: String) {
        interactor?.login(with: username, password: password)
    }

    func didAuthenticateUser(success: Bool) {
        if success {
            view?.showAlert(message: "登录成功!")
        } else {
            view?.showAlert(message: "用户名或密码错误。")
        }
    }
}

最后,如果登录成功,我们可能希望跳转到另一个页面。所以,我们在LoginRouterProtocol中添加一个新方法来处理跳转逻辑:

protocol LoginRouterProtocol: AnyObject {
    static func createLoginModule() -> UIViewController

    func navigateToHome(from view: LoginViewProtocol)
}

然后,在LoginRouter中实现该方法:

class LoginRouter: LoginRouterProtocol {
    // ...其他代码...

    func navigateToHome(from view: LoginViewProtocol) {
        // 跳转到主页面的代码,具体取决于你的应用结构
    }
}

这样,我们就完成了对登录功能的VIPER架构的拓展。希望这个示例能帮助你理解如何在实际项目中使用VIPER架构。

实现组件

为了实现你的需求,我们可以使用 UITextView 来替代 UILabel,并设置属性以便将一部分文本作为可点击的链接。以下是一种可能的解决方案:

import UIKit

class CheckBoxView: UIView {
    private let checkBoxButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("☐", for: .normal)
        button.setTitle("☑️", for: .selected)
        return button
    }()

    // 使用 UITextView 替代 UILabel
    private let textView: UITextView = {
        let view = UITextView()
        view.isEditable = false // 设为不可编辑
        return view
    }()

    init(text: String, linkRange: NSRange, url: URL) {
        super.init(frame: .zero)

        let attributedString = NSMutableAttributedString(string: text)
        attributedString.addAttribute(.link, value: url, range: linkRange)
        textView.attributedText = attributedString

        addSubview(checkBoxButton)
        addSubview(textView)

        checkBoxButton.addTarget(self, action: #selector(checkBoxTapped), for: .touchUpInside)

        setupConstraints()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupConstraints() {
        checkBoxButton.translatesAutoresizingMaskIntoConstraints = false
        textView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            checkBoxButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            checkBoxButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),

            textView.leadingAnchor.constraint(equalTo: checkBoxButton.trailingAnchor, constant: 10),
            textView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            textView.topAnchor.constraint(equalTo: self.topAnchor),
            textView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
    }

    @objc private func checkBoxTapped() {
        checkBoxButton.isSelected = !checkBoxButton.isSelected
    }
}

在这段代码中,我们首先将 UILabel 替换为 UITextView,并将其设置为不可编辑。然后在初始化方法中,我们创建一个 NSMutableAttributedString 并将链接属性应用于所需的文本范围。最终,通过对 textView.attributedText 属性赋值,使得用户可以点击URL链接。

你可以通过 let view = CheckBoxView(text: "你需要的文字", linkRange: NSMakeRange(startIndex, length), url: URL(string: "http://example.com")!) 来使用这个自定义的视图,其中 startIndexlength 需要替换成实际文字中需要链接的部分的开始索引和长度。