mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-20 20:06:04 -06:00
Fixes #1305. ## Summary Enables the new-to-macOS 11 interface idiom of Mac, which removes any interface scaling and brings more Mac-like UI. ## Screenshots <img width="350" alt="image" src="https://user-images.githubusercontent.com/74188/115946708-ea115500-a477-11eb-9e42-378e76d6e136.png"><img width="350" alt="image" src="https://user-images.githubusercontent.com/74188/115946711-eed60900-a477-11eb-8591-fd01a3711787.png"> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes - Updates Settings on Big Sur to use a toolbar to swap between root-level settings screens. - Fixes blurry web view, only on Big Sur. - `UISwitch` as a checkbox looks kind of weird; may need to do some Eureka-level changes to make SwitchRow use the `title` property on `UISwitch` to make it connected so the whole thing is clickable. This gets weird because the font size is larger for this property.
370 lines
13 KiB
Swift
370 lines
13 KiB
Swift
import CoreLocation
|
|
import Eureka
|
|
import Foundation
|
|
import PromiseKit
|
|
import Shared
|
|
|
|
final class ConnectionURLViewController: HAFormViewController, TypedRowControllerType {
|
|
typealias RowValue = ConnectionURLViewController
|
|
var row: RowOf<RowValue>!
|
|
var onDismissCallback: ((UIViewController) -> Void)?
|
|
let urlType: ConnectionInfo.URLType
|
|
|
|
init(urlType: ConnectionInfo.URLType, row: RowOf<RowValue>) {
|
|
self.urlType = urlType
|
|
self.row = row
|
|
|
|
super.init()
|
|
|
|
self.title = urlType.description
|
|
|
|
if #available(iOS 13, *) {
|
|
self.isModalInPresentation = true
|
|
}
|
|
}
|
|
|
|
enum SaveError: LocalizedError {
|
|
case lastURL
|
|
case validation([ValidationError])
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .lastURL: return L10n.Settings.ConnectionSection.Errors.cannotRemoveLastUrl
|
|
case let .validation(errors): return errors.map(\.msg).joined(separator: "\n")
|
|
}
|
|
}
|
|
|
|
var isFinal: Bool {
|
|
switch self {
|
|
case .lastURL: return true
|
|
case .validation: return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func check(url: URL?, useCloud: Bool?, validationErrors: [ValidationError]) throws {
|
|
if !validationErrors.isEmpty {
|
|
throw SaveError.validation(validationErrors)
|
|
}
|
|
|
|
if url == nil, let existingInfo = Current.settingsStore.connectionInfo {
|
|
let other: ConnectionInfo.URLType = urlType == .internal ? .external : .internal
|
|
if existingInfo.address(for: other) == nil,
|
|
useCloud == false || (useCloud == nil && !existingInfo.useCloud) {
|
|
throw SaveError.lastURL
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func cancel() {
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
@objc private func save() {
|
|
let givenURL = (form.rowBy(tag: RowTag.url.rawValue) as? URLRow)?.value
|
|
let useCloud = (form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow)?.value
|
|
|
|
func commit() {
|
|
Current.settingsStore.connectionInfo?.setAddress(givenURL, urlType)
|
|
|
|
if let useCloud = useCloud {
|
|
Current.settingsStore.connectionInfo?.useCloud = useCloud
|
|
}
|
|
|
|
if let section = form.sectionBy(tag: RowTag.ssids.rawValue) as? MultivaluedSection {
|
|
Current.settingsStore.connectionInfo?.internalSSIDs = section.allRows
|
|
.compactMap { $0 as? TextRow }
|
|
.compactMap(\.value)
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
if let section = form.sectionBy(tag: RowTag.hardwareAddresses.rawValue) as? MultivaluedSection {
|
|
Current.settingsStore.connectionInfo?.internalHardwareAddresses = section.allRows
|
|
.compactMap { $0 as? TextRow }
|
|
.compactMap(\.value)
|
|
.map { $0.lowercased() }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
updateNavigationItems(isChecking: true)
|
|
|
|
firstly { () -> Promise<Void> in
|
|
try check(url: givenURL, useCloud: useCloud, validationErrors: form.validate())
|
|
|
|
if useCloud == true, let url = Current.settingsStore.connectionInfo?.remoteUIURL {
|
|
return Current.webhooks.sendTest(baseURL: url)
|
|
}
|
|
|
|
if let givenURL = givenURL, useCloud != true {
|
|
return Current.webhooks.sendTest(baseURL: givenURL)
|
|
}
|
|
|
|
return .value(())
|
|
}.ensure {
|
|
self.updateNavigationItems(isChecking: false)
|
|
}.done {
|
|
commit()
|
|
}.catch { error in
|
|
let alert = UIAlertController(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.title,
|
|
message: error.localizedDescription,
|
|
preferredStyle: .alert
|
|
)
|
|
|
|
let canCommit: Bool
|
|
|
|
if let error = error as? SaveError {
|
|
canCommit = !error.isFinal
|
|
} else {
|
|
canCommit = true
|
|
}
|
|
|
|
if canCommit {
|
|
alert.addAction(UIAlertAction(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.useAnyway,
|
|
style: .default,
|
|
handler: { _ in commit() }
|
|
))
|
|
}
|
|
|
|
alert.addAction(UIAlertAction(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.editUrl,
|
|
style: .cancel,
|
|
handler: nil
|
|
))
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
fileprivate enum RowTag: String {
|
|
case url
|
|
case ssids
|
|
case hardwareAddresses
|
|
case useCloud
|
|
}
|
|
|
|
private func updateNavigationItems(isChecking: Bool) {
|
|
navigationItem.leftBarButtonItems = [
|
|
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)),
|
|
]
|
|
|
|
if isChecking {
|
|
let activityIndicator: UIActivityIndicatorView
|
|
|
|
if #available(iOS 13, *) {
|
|
activityIndicator = .init(style: .medium)
|
|
} else {
|
|
activityIndicator = .init(style: .gray)
|
|
}
|
|
|
|
activityIndicator.startAnimating()
|
|
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(customView: activityIndicator),
|
|
]
|
|
} else {
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(save)),
|
|
]
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
updateNavigationItems(isChecking: false)
|
|
|
|
if urlType.isAffectedByCloud, Current.settingsStore.connectionInfo?.canUseCloud == true {
|
|
form +++ SwitchRow {
|
|
$0.title = L10n.Settings.ConnectionSection.HomeAssistantCloud.title
|
|
$0.tag = RowTag.useCloud.rawValue
|
|
$0.value = Current.settingsStore.connectionInfo?.useCloud
|
|
}
|
|
}
|
|
|
|
form +++ Section()
|
|
<<< URLRow(RowTag.url.rawValue) {
|
|
$0.value = Current.settingsStore.connectionInfo?.address(for: urlType)
|
|
$0.hidden = .function([RowTag.useCloud.rawValue], { form in
|
|
if let row = form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow {
|
|
// if cloud's around, hide when it's turned on
|
|
return row.value == true
|
|
} else {
|
|
// never hide if cloud isn't around
|
|
return false
|
|
}
|
|
})
|
|
$0.placeholder = { () -> String? in
|
|
switch urlType {
|
|
case .internal: return L10n.Settings.ConnectionSection.InternalBaseUrl.placeholder
|
|
case .external: return L10n.Settings.ConnectionSection.ExternalBaseUrl.placeholder
|
|
case .remoteUI: return nil
|
|
}
|
|
}()
|
|
}
|
|
|
|
<<< InfoLabelRow {
|
|
$0.title = L10n.Settings.ConnectionSection.cloudOverridesExternal
|
|
$0.hidden = .function([RowTag.useCloud.rawValue], { form in
|
|
if let row = form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow {
|
|
// this is effectively the visual replacement for the external url, so show when cloud is on
|
|
return row.value == false
|
|
} else {
|
|
// always hide if we're not offering the cloud option
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
if urlType.isAffectedBySSID {
|
|
form +++ locationPermissionSection()
|
|
|
|
form +++ MultivaluedSection(
|
|
tag: .ssids,
|
|
header: L10n.Settings.ConnectionSection.InternalUrlSsids.header,
|
|
footer: L10n.Settings.ConnectionSection.InternalUrlSsids.footer,
|
|
addNewLabel: L10n.Settings.ConnectionSection.InternalUrlSsids.addNewSsid,
|
|
placeholder: L10n.Settings.ConnectionSection.InternalUrlSsids.placeholder,
|
|
currentValue: Current.connectivity.currentWiFiSSID,
|
|
existingValues: Current.settingsStore.connectionInfo?.internalSSIDs ?? [],
|
|
valueRules: RuleSet<String>()
|
|
)
|
|
}
|
|
|
|
if urlType.isAffectedByHardwareAddress {
|
|
var rules = RuleSet<String>()
|
|
rules.add(rule: RuleRegExp(
|
|
regExpr: "^[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}$",
|
|
allowsEmpty: true,
|
|
msg: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.invalid,
|
|
id: nil
|
|
))
|
|
|
|
form +++ MultivaluedSection(
|
|
tag: .hardwareAddresses,
|
|
header: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.header,
|
|
footer: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.footer,
|
|
addNewLabel: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.addNewSsid,
|
|
placeholder: "aa:bb:cc:dd:ee:ff",
|
|
currentValue: Current.connectivity.currentNetworkHardwareAddress,
|
|
existingValues: Current.settingsStore.connectionInfo?.internalHardwareAddresses ?? [],
|
|
valueRules: rules
|
|
)
|
|
}
|
|
}
|
|
|
|
private func locationPermissionSection() -> Section {
|
|
class PermissionWatchingDelegate: NSObject, CLLocationManagerDelegate {
|
|
let section: Section
|
|
|
|
init(section: Section) {
|
|
self.section = section
|
|
}
|
|
|
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
section.evaluateHidden()
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
|
section.evaluateHidden()
|
|
}
|
|
}
|
|
|
|
let section = Section()
|
|
var locationManager: CLLocationManager? = CLLocationManager()
|
|
var permissionDelegate: PermissionWatchingDelegate? = PermissionWatchingDelegate(section: section)
|
|
locationManager?.delegate = permissionDelegate
|
|
|
|
section.hidden = .function([], { _ in
|
|
if #available(iOS 14, *), let locationManager = locationManager {
|
|
return locationManager.authorizationStatus == .authorizedAlways &&
|
|
locationManager.accuracyAuthorization == .fullAccuracy
|
|
} else {
|
|
return CLLocationManager.authorizationStatus() == .authorizedAlways
|
|
}
|
|
})
|
|
section.evaluateHidden()
|
|
|
|
after(life: self).done {
|
|
// we're keeping these lifetimes around longer so they update
|
|
locationManager = nil
|
|
permissionDelegate = nil
|
|
}
|
|
|
|
section <<< InfoLabelRow {
|
|
if #available(iOS 14, *) {
|
|
$0.title = L10n.Settings.ConnectionSection.ssidPermissionAndAccuracyMessage
|
|
} else {
|
|
$0.title = L10n.Settings.ConnectionSection.ssidPermissionMessage
|
|
}
|
|
|
|
$0.cellUpdate { cell, _ in
|
|
cell.accessibilityTraits.insert(.button)
|
|
cell.selectionStyle = .default
|
|
}
|
|
|
|
$0.onCellSelection { _, _ in
|
|
if CLLocationManager.authorizationStatus() == .notDetermined {
|
|
locationManager?.requestAlwaysAuthorization()
|
|
} else {
|
|
UIApplication.shared.openSettings(destination: .location)
|
|
}
|
|
}
|
|
}
|
|
|
|
return section
|
|
}
|
|
}
|
|
|
|
private extension MultivaluedSection {
|
|
convenience init(
|
|
tag: ConnectionURLViewController.RowTag,
|
|
header: String,
|
|
footer: String,
|
|
addNewLabel: String,
|
|
placeholder: String,
|
|
currentValue: @escaping () -> String?,
|
|
existingValues: [String],
|
|
valueRules: RuleSet<String>
|
|
) {
|
|
self.init(
|
|
multivaluedOptions: [.Insert, .Delete],
|
|
header: header,
|
|
footer: footer
|
|
) { section in
|
|
section.tag = tag.rawValue
|
|
section.addButtonProvider = { _ in
|
|
ButtonRow {
|
|
$0.title = addNewLabel
|
|
}.cellUpdate { cell, _ in
|
|
cell.textLabel?.textAlignment = .natural
|
|
cell.selectionStyle = .default
|
|
}
|
|
}
|
|
|
|
func row(for value: String?) -> TextRow {
|
|
TextRow {
|
|
$0.placeholder = placeholder
|
|
$0.value = value
|
|
$0.add(ruleSet: valueRules)
|
|
}
|
|
}
|
|
|
|
section.multivaluedRowToInsertAt = { _ in
|
|
let current = currentValue()
|
|
|
|
if section.allRows.contains(where: { ($0 as? TextRow)?.value == current }) {
|
|
return row(for: nil)
|
|
} else {
|
|
return row(for: current)
|
|
}
|
|
}
|
|
|
|
section.append(contentsOf: existingValues.map { row(for: $0) })
|
|
}
|
|
}
|
|
}
|