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
相关产品推荐
相关产品推荐

