// SPDX-License-Identifier: MIT // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. import UIKit class TunnelListCell: UITableViewCell { var tunnel: TunnelContainer? { didSet { // Bind to the tunnel's name nameLabel.text = tunnel?.name ?? "" nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in self?.nameLabel.text = tunnel.name } // Bind to the tunnel's status update(from: tunnel?.status, animated: false) statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in self?.update(from: tunnel.status, animated: true) } } } var onSwitchToggled: ((Bool) -> Void)? let nameLabel: UILabel = { let nameLabel = UILabel() nameLabel.font = UIFont.preferredFont(forTextStyle: .body) nameLabel.adjustsFontForContentSizeCategory = true nameLabel.numberOfLines = 0 return nameLabel }() let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView if #available(iOS 13.0, *) { busyIndicator = UIActivityIndicatorView(style: .medium) } else { busyIndicator = UIActivityIndicatorView(style: .gray) } busyIndicator.hidesWhenStopped = true return busyIndicator }() let statusSwitch = UISwitch() private var statusObservationToken: NSKeyValueObservation? private var nameObservationToken: NSKeyValueObservation? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) accessoryType = .disclosureIndicator for subview in [statusSwitch, busyIndicator, nameLabel] { subview.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(subview) } nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let nameLabelBottomConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1) nameLabelBottomConstraint.priority = .defaultLow NSLayoutConstraint.activate([ statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), statusSwitch.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1), nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1), nameLabelBottomConstraint, nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1), busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), busyIndicator.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1) ]) statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() reset(animated: false) } override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) statusSwitch.isEnabled = !editing } @objc private func switchToggled() { onSwitchToggled?(statusSwitch.isOn) } private func update(from status: TunnelStatus?, animated: Bool) { guard let status = status else { reset(animated: animated) return } statusSwitch.setOn(!(status == .deactivating || status == .inactive), animated: animated) statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active) if status == .inactive || status == .active { busyIndicator.stopAnimating() } else { busyIndicator.startAnimating() } } private func reset(animated: Bool) { statusSwitch.setOn(false, animated: animated) statusSwitch.isUserInteractionEnabled = false busyIndicator.stopAnimating() } }