import UIKit /// Asks where ipx is. Shown on first run, and again whenever the address needs changing. final class ServerSetupViewController: UIViewController, UITextFieldDelegate { var onSave: ((URL) -> Void)? private let field = UITextField() private let note = UILabel() private let first: Bool /// The first run has nothing to go back to, so it gets no Cancel. init(first: Bool) { self.first = first super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("not from a storyboard") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground title = first ? "Where is ipx?" : "Server" navigationItem.rightBarButtonItem = UIBarButtonItem( title: "Save", style: .done, target: self, action: #selector(save)) if !first { navigationItem.leftBarButtonItem = UIBarButtonItem( title: "Cancel", style: .plain, target: self, action: #selector(cancel)) } let blurb = UILabel() blurb.text = "The address of your ipx server. Sign in on the page that follows; the app plays " + "what you are signed in to." blurb.numberOfLines = 0 blurb.font = .preferredFont(forTextStyle: .subheadline) blurb.textColor = .secondaryLabel field.text = ServerSettings.base?.absoluteString ?? ServerSettings.suggestion field.placeholder = ServerSettings.suggestion field.borderStyle = .roundedRect field.keyboardType = .URL field.textContentType = .URL field.autocapitalizationType = .none field.autocorrectionType = .no field.spellCheckingType = .no field.clearButtonMode = .whileEditing field.returnKeyType = .done field.delegate = self field.addTarget(self, action: #selector(edited), for: .editingChanged) note.numberOfLines = 0 note.font = .preferredFont(forTextStyle: .footnote) note.textColor = .secondaryLabel let stack = UIStackView(arrangedSubviews: [blurb, field, note]) stack.axis = .vertical stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), ]) edited() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) field.becomeFirstResponder() } func textFieldShouldReturn(_ textField: UITextField) -> Bool { save(); return true } /// Says what is wrong while it is still being typed, rather than after a failed load. @objc private func edited() { let typed = field.text ?? "" guard let url = ServerSettings.parse(typed) else { note.text = typed.isEmpty ? "" : "That is not a web address." note.textColor = .secondaryLabel navigationItem.rightBarButtonItem?.isEnabled = false return } navigationItem.rightBarButtonItem?.isEnabled = true if url.scheme == "http" && !ServerSettings.isLocal(url) { // Saying so here beats a blocked request later that reads like the server is down. note.text = "Plain HTTP is only allowed on your own network. This address will not load." note.textColor = .systemOrange } else if url.scheme == "http" { note.text = "On your own network, so plain HTTP is fine." note.textColor = .secondaryLabel } else { note.text = "Will open \(url.absoluteString)" note.textColor = .secondaryLabel } } @objc private func save() { guard let url = ServerSettings.parse(field.text ?? "") else { return } ServerSettings.base = url field.resignFirstResponder() onSave?(url) } @objc private func cancel() { dismiss(animated: true) } }