多Target项目代码共享架构咨询:登录页Protocol实现方案探讨
Great question! Building a shared codebase for multiple targets while retaining per-target customization is a common (and smart) goal for scalable iOS projects. I’ve used this exact pattern on several apps, and here’s the most maintainable approach I’ve found:
The key is to separate shared contract/behavior from target-specific logic:
- Use a protocol to define the mandatory methods/properties all login controllers must implement (this is your "contract").
- Create a base view controller that provides reusable UI, common validation, and default behavior (so you don’t repeat boilerplate across targets).
- Have each target’s login controller inherit from the base class and conform to the protocol, overriding only what’s unique to that target.
Organize your project into clear modules to keep shared code isolated and easy to reference across targets:
YourProject/ ├── Shared/ # Reusable code for all targets │ ├── Login/ │ │ ├── LoginProtocol.swift # Defines the login controller contract │ │ ├── BaseLoginViewController.swift # Shared UI/behavior │ │ ├── LoginModels.swift # Shared data types (e.g., UserInfo, LoginError) │ │ └── LoginValidators.swift # Common validation logic ├── TargetA/ # First target-specific code │ └── Login/ │ └── TargetALoginViewController.swift # TargetA's custom login flow └── TargetB/ # Second target-specific code └── Login/ └── TargetBLoginViewController.swift # TargetB's custom login flow
Step 1: Define the Protocol
Start with a protocol that outlines what every login controller needs to do. Use extensions to add default implementations for shared logic:
// Shared/Login/LoginProtocol.swift import UIKit enum LoginError: LocalizedError { case invalidCredentials case networkFailure var errorDescription: String? { switch self { case .invalidCredentials: return "Please enter a valid username and password." case .networkFailure: return "Could not connect to the server. Try again later." } } } struct UserInfo { let id: String let name: String } protocol LoginProtocol: AnyObject { // Mandatory UI components var usernameTextField: UITextField! { get } var passwordTextField: UITextField! { get } // Mandatory methods (target-specific logic) func handleLoginButtonTap() func showLoginSuccess(for user: UserInfo) // Optional method with default implementation func validateCredentials() -> Bool } extension LoginProtocol { // Default validation logic (targets can override this) func validateCredentials() -> Bool { guard let username = usernameTextField.text, !username.isEmpty else { return false } guard let password = passwordTextField.text, password.count >= 6 else { return false } return true } // Default error presentation (targets can override) func showLoginError(_ error: LoginError) { let alert = UIAlertController(title: "Login Failed", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) (self as? UIViewController)?.present(alert, animated: true) } }
Step 2: Build the Base View Controller
This class handles all shared UI setup and boilerplate, so targets only need to add their unique logic:
// Shared/Login/BaseLoginViewController.swift import UIKit class BaseLoginViewController: UIViewController, LoginProtocol { // MARK: - Protocol Conformance var usernameTextField: UITextField! var passwordTextField: UITextField! // MARK: - Shared UI Components private let loginButton = UIButton(type: .system) override func viewDidLoad() { super.viewDidLoad() setupSharedUI() setupSharedConstraints() } // MARK: - Shared Setup private func setupSharedUI() { view.backgroundColor = .systemBackground usernameTextField = UITextField() usernameTextField.placeholder = "Username" usernameTextField.borderStyle = .roundedRect view.addSubview(usernameTextField) passwordTextField = UITextField() passwordTextField.placeholder = "Password" passwordTextField.borderStyle = .roundedRect passwordTextField.isSecureTextEntry = true view.addSubview(passwordTextField) loginButton.setTitle("Login", for: .normal) loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) view.addSubview(loginButton) } private func setupSharedConstraints() { usernameTextField.translatesAutoresizingMaskIntoConstraints = false passwordTextField.translatesAutoresizingMaskIntoConstraints = false loginButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ usernameTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor), usernameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40), usernameTextField.widthAnchor.constraint(equalToConstant: 280), passwordTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor), passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 20), passwordTextField.widthAnchor.constraint(equalToConstant: 280), loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 30) ]) } // MARK: - Shared Behavior @objc private func loginButtonTapped() { guard validateCredentials() else { showLoginError(.invalidCredentials) return } handleLoginButtonTap() // Delegates to target-specific implementation } // MARK: - Mandatory Protocol Methods (Must Be Overridden) func handleLoginButtonTap() { fatalError("Subclasses must implement handleLoginButtonTap()") } func showLoginSuccess(for user: UserInfo) { fatalError("Subclasses must implement showLoginSuccess(for:)") } }
Step 3: Implement Target-Specific Logic
For each target, create a subclass of BaseLoginViewController and override only what’s unique:
// TargetA/Login/TargetALoginViewController.swift import UIKit class TargetALoginViewController: BaseLoginViewController { // TargetA-specific UI component private let appleLoginButton = UIButton(type: .system) override func viewDidLoad() { super.viewDidLoad() setupTargetSpecificUI() } private func setupTargetSpecificUI() { appleLoginButton.setTitle("Login with Apple", for: .normal) appleLoginButton.addTarget(self, action: #selector(appleLoginTapped), for: .touchUpInside) view.addSubview(appleLoginButton) appleLoginButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ appleLoginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), appleLoginButton.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 20) ]) } // MARK: - Override Protocol Methods for TargetA Logic override func handleLoginButtonTap() { // Call TargetA's unique authentication API TargetAAuthService.login(username: usernameTextField.text!, password: passwordTextField.text!) { [weak self] result in DispatchQueue.main.async { switch result { case .success(let user): self?.showLoginSuccess(for: user) case .failure(let error): self?.showLoginError(error) } } } } override func showLoginSuccess(for user: UserInfo) { let alert = UIAlertController(title: "Welcome, \(user.name)!", message: "Logged in via TargetA", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Go to Home", style: .default) { _ in self.navigationController?.pushViewController(TargetAHomeViewController(), animated: true) }) present(alert, animated: true) } // MARK: - Target-Specific Action @objc private func appleLoginTapped() { // Implement TargetA's Apple Sign-In flow TargetAAppleAuthService.startAuthentication { result in // Handle result } } }
- Dependency Injection: Pass target-specific services (like auth services) into the base controller via initializers, making shared code more testable and flexible.
- Modularization: Package the
Shareddirectory into a Swift Package or Framework, so you can easily reuse it across multiple projects or targets. - Protocol Extensions: Add more default behavior to
LoginProtocol(e.g.,showLoadingSpinner()) to reduce repetitive code. - Unit Testing: Use protocols to mock dependencies (like auth services) when testing shared logic, ensuring your base code works as expected without relying on target-specific APIs.
内容的提问来源于stack exchange,提问作者Andrea Mario Lufino

