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传递给Presenter,Presenter调用Interactor来完成核心业务逻辑,并更新View。Router则负责处理视图跳转逻辑。各个组件之间的耦合度降低,可以独立进行单元测试,代码也更容易理解和维护。
注意: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)
}
最后,我们在LoginInteractor和LoginPresenter中实现新的方法:
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")!) 来使用这个自定义的视图,其中 startIndex 和 length 需要替换成实际文字中需要链接的部分的开始索引和长度。