You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift编程式约束学习:视图类文件中引用父视图的代码问题

解决Swift自定义视图中引用父视图控制器的正确姿势

嘿,我明白你现在在搞Swift编程式约束和代码模块化拆分的痛点,尤其是自定义视图和父视图控制器之间的引用问题——直接强引用很容易踩内存泄漏的坑,我给你捋几个安全又符合iOS最佳实践的方案:

首先要避开的坑:不要直接强引用父VC

千万别在你的LoginView里这么写:

// 错误示例!会导致循环引用内存泄漏
var parentVC: LoginViewController!

因为LoginViewController持有loginView,如果loginView又持有LoginViewController,就会形成循环引用,两者都无法被系统回收,造成内存浪费。


方案一:用代理模式(iOS官方推荐的解耦方式)

这种方式能很好地把UI视图(LoginView)和业务逻辑(LoginViewController)拆分开,保持代码模块化:

第一步:在LoginView里定义代理协议

import UIKit

// 定义代理协议,声明视图需要通知VC的事件
protocol LoginViewDelegate: AnyObject {
    func loginViewDidTapLoginButton(_ view: LoginView, username: String?, password: String?)
    func loginViewDidTapForgotPasswordButton(_ view: LoginView)
}

class LoginView: UIView {
    // 用weak修饰代理,避免循环引用
    weak var delegate: LoginViewDelegate?
    
    // 假设你有用户名输入框、密码输入框、登录按钮
    private let usernameTextField = UITextField()
    private let passwordTextField = UITextField()
    private let loginButton = UIButton(type: .system)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupUI() {
        // 这里放你的样式设置:比如背景色、输入框样式、按钮标题等
        backgroundColor = .white
        loginButton.setTitle("登录", for: .normal)
        loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
        
        // 添加子视图到当前view
        addSubview(usernameTextField)
        addSubview(passwordTextField)
        addSubview(loginButton)
    }
    
    private func setupConstraints() {
        // 编程式约束(这里用SnapKit为例,你用原生约束也一样)
        usernameTextField.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(100)
            make.leading.trailing.equalToSuperview().inset(20)
            make.height.equalTo(44)
        }
        
        passwordTextField.snp.makeConstraints { make in
            make.top.equalTo(usernameTextField.snp.bottom).offset(20)
            make.leading.trailing.height.equalTo(usernameTextField)
        }
        
        loginButton.snp.makeConstraints { make in
            make.top.equalTo(passwordTextField.snp.bottom).offset(30)
            make.leading.trailing.equalToSuperview().inset(20)
            make.height.equalTo(44)
        }
    }
    
    @objc private func loginButtonTapped() {
        let username = usernameTextField.text
        let password = passwordTextField.text
        // 通知代理(也就是父VC)处理登录逻辑
        delegate?.loginViewDidTapLoginButton(self, username: username, password: password)
    }
}

第二步:在LoginViewController里遵守并实现代理

import UIKit

class LoginViewController: UIViewController, LoginViewDelegate {
    var loginView: LoginView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLoginView()
    }
    
    private func setupLoginView() {
        loginView = LoginView(frame: .zero)
        // 设置代理为当前VC
        loginView.delegate = self
        view.addSubview(loginView)
        
        // 把LoginView钉死在父视图边缘(你原来的代码补全)
        loginView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
    
    // 实现代理方法,处理登录逻辑
    func loginViewDidTapLoginButton(_ view: LoginView, username: String?, password: String?) {
        guard let username = username, let password = password, !username.isEmpty, !password.isEmpty else {
            // 提示用户输入完整信息
            let alert = UIAlertController(title: "提示", message: "请输入用户名和密码", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "确定", style: .default))
            present(alert, animated: true)
            return
        }
        // 这里写你的网络请求或者登录逻辑
        print("开始登录:用户名\(username),密码\(password)")
    }
    
    func loginViewDidTapForgotPasswordButton(_ view: LoginView) {
        // 处理忘记密码逻辑,比如跳转到忘记密码页面
        print("忘记密码按钮被点击")
    }
}

方案二:用闭包回调(更简洁的轻量级方式)

如果你的事件不多,闭包会比代理更简洁,同样能避免循环引用:

修改LoginView的代码,添加闭包属性

class LoginView: UIView {
    // 定义登录按钮点击的闭包,用@escaping因为闭包会在方法结束后执行
    var onLoginButtonTap: ((String?, String?) -> Void)?
    var onForgotPasswordTap: (() -> Void)?
    
    // ... 其他UI设置和约束代码和上面一样 ...
    
    @objc private func loginButtonTapped() {
        let username = usernameTextField.text
        let password = passwordTextField.text
        // 触发闭包
        onLoginButtonTap?(username, password)
    }
    
    @objc private func forgotPasswordButtonTapped() {
        onForgotPasswordTap?()
    }
}

在LoginViewController里赋值闭包

private func setupLoginView() {
    loginView = LoginView(frame: .zero)
    view.addSubview(loginView)
    loginView.snp.makeConstraints { make in
        make.edges.equalToSuperview()
    }
    
    // 赋值登录闭包,注意用[weak self]避免循环引用
    loginView.onLoginButtonTap = { [weak self] username, password in
        guard let self = self else { return }
        guard let username = username, let password = password, !username.isEmpty, !password.isEmpty else {
            let alert = UIAlertController(title: "提示", message: "请输入用户名和密码", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "确定", style: .default))
            self.present(alert, animated: true)
            return
        }
        print("开始登录:用户名\(username),密码\(password)")
        // 这里可以调用VC的登录方法
        self.performLogin(username: username, password: password)
    }
    
    loginView.onForgotPasswordTap = { [weak self] in
        guard let self = self else { return }
        print("忘记密码按钮被点击")
        // 跳转到忘记密码页面
        self.navigateToForgotPasswordVC()
    }
}

// VC里的业务方法,保持逻辑分离
private func performLogin(username: String, password: String) {
    // 网络请求等逻辑
}

private func navigateToForgotPasswordVC() {
    // 页面跳转逻辑
}

如果只是需要访问父视图(不是VC)

如果你只是需要在LoginView里访问它的父UIView(不是ViewController),可以直接用self.superview,但要注意它可能为nil,所以一定要用可选绑定:

// 在LoginView的某个方法里
if let parentView = self.superview {
    // 对父视图做操作,比如修改背景色
    parentView.backgroundColor = .lightGray
}

这样拆分之后,LoginView只负责UI的布局、样式和事件触发,LoginViewController负责业务逻辑和页面跳转,完美符合你想要的代码整洁与模块化的需求!

内容的提问来源于stack exchange,提问作者Nappstir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:53:24