199 lines
7.6 KiB
Swift
199 lines
7.6 KiB
Swift
import UIKit
|
|
|
|
@MainActor
|
|
final class MilestoneEditorViewController: UIViewController, UITextFieldDelegate {
|
|
private let context: AppContext
|
|
private let owner: String
|
|
private let repository: String
|
|
private let id: Int64?
|
|
private let saved: (Int64) -> Void
|
|
private let scrollView = UIScrollView()
|
|
private let stack = UIStackView()
|
|
private let titleField = UITextField()
|
|
private let descriptionView = UITextView()
|
|
private let dueSwitch = UISwitch()
|
|
private let duePicker = UIDatePicker()
|
|
private let spinner = UIActivityIndicatorView(style: .medium)
|
|
private var task: Task<Void, Never>?
|
|
|
|
init(
|
|
context: AppContext,
|
|
owner: String,
|
|
repository: String,
|
|
id: Int64? = nil,
|
|
saved: @escaping (Int64) -> Void
|
|
) {
|
|
self.context = context
|
|
self.owner = owner
|
|
self.repository = repository
|
|
self.id = id
|
|
self.saved = saved
|
|
super.init(nibName: nil, bundle: nil)
|
|
title = id == nil ? "New Milestone" : "Edit Milestone"
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
|
|
|
deinit { task?.cancel() }
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .systemGroupedBackground
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
|
barButtonSystemItem: .cancel,
|
|
target: self,
|
|
action: #selector(cancel)
|
|
)
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
|
|
spinner.startAnimating()
|
|
configureForm()
|
|
task = Task { await load() }
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
if id == nil { titleField.becomeFirstResponder() }
|
|
}
|
|
|
|
private func configureForm() {
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.keyboardDismissMode = .interactive
|
|
view.addSubview(scrollView)
|
|
stack.axis = .vertical
|
|
stack.spacing = 12
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
stack.isHidden = true
|
|
scrollView.addSubview(stack)
|
|
|
|
titleField.placeholder = "Title"
|
|
titleField.delegate = self
|
|
titleField.font = .preferredFont(forTextStyle: .headline)
|
|
titleField.adjustsFontForContentSizeCategory = true
|
|
titleField.borderStyle = .roundedRect
|
|
titleField.autocapitalizationType = .sentences
|
|
titleField.returnKeyType = .next
|
|
titleField.accessibilityLabel = "Milestone title"
|
|
titleField.addTarget(self, action: #selector(titleChanged), for: .editingChanged)
|
|
|
|
let descriptionLabel = UILabel()
|
|
descriptionLabel.text = "Description"
|
|
descriptionLabel.font = .preferredFont(forTextStyle: .headline)
|
|
descriptionLabel.adjustsFontForContentSizeCategory = true
|
|
descriptionView.font = .preferredFont(forTextStyle: .body)
|
|
descriptionView.adjustsFontForContentSizeCategory = true
|
|
descriptionView.backgroundColor = .secondarySystemGroupedBackground
|
|
descriptionView.layer.cornerRadius = 10
|
|
descriptionView.textContainerInset = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 8)
|
|
descriptionView.autocapitalizationType = .sentences
|
|
descriptionView.accessibilityLabel = "Milestone description"
|
|
|
|
let dueLabel = UILabel()
|
|
dueLabel.text = "Due Date"
|
|
dueLabel.font = .preferredFont(forTextStyle: .body)
|
|
dueLabel.adjustsFontForContentSizeCategory = true
|
|
let dueRow = UIStackView(arrangedSubviews: [dueLabel, dueSwitch])
|
|
dueRow.alignment = .center
|
|
dueSwitch.accessibilityLabel = "Set due date"
|
|
dueSwitch.addTarget(self, action: #selector(dueChanged), for: .valueChanged)
|
|
duePicker.datePickerMode = .date
|
|
duePicker.preferredDatePickerStyle = .inline
|
|
duePicker.isHidden = true
|
|
|
|
stack.addArrangedSubview(titleField)
|
|
stack.addArrangedSubview(descriptionLabel)
|
|
stack.addArrangedSubview(descriptionView)
|
|
stack.addArrangedSubview(dueRow)
|
|
stack.addArrangedSubview(duePicker)
|
|
NSLayoutConstraint.activate([
|
|
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
|
|
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
|
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
|
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
|
|
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
|
|
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
|
|
descriptionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 180),
|
|
])
|
|
}
|
|
|
|
private func load() async {
|
|
do {
|
|
let page = try await context.core.milestoneEditor(
|
|
owner: owner,
|
|
repository: repository,
|
|
id: id
|
|
)
|
|
guard !Task.isCancelled else { return }
|
|
titleField.text = page.title
|
|
descriptionView.text = page.description
|
|
if let timestamp = page.dueDate {
|
|
dueSwitch.isOn = true
|
|
duePicker.date = localDate(forUTCTimestamp: timestamp)
|
|
duePicker.isHidden = false
|
|
}
|
|
stack.isHidden = false
|
|
} catch {
|
|
if !Task.isCancelled { show(error: error) }
|
|
}
|
|
restoreSaveButton()
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
descriptionView.becomeFirstResponder()
|
|
return true
|
|
}
|
|
|
|
@objc private func dueChanged() {
|
|
duePicker.isHidden = !dueSwitch.isOn
|
|
}
|
|
|
|
@objc private func titleChanged() {
|
|
navigationItem.rightBarButtonItem?.isEnabled = !(titleField.text ?? "")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
|
|
@objc private func save() {
|
|
view.endEditing(true)
|
|
navigationItem.leftBarButtonItem?.isEnabled = false
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
|
|
spinner.startAnimating()
|
|
task?.cancel()
|
|
task = Task {
|
|
do {
|
|
let id = try await context.core.saveMilestone(
|
|
owner: owner,
|
|
repository: repository,
|
|
id: id,
|
|
title: titleField.text ?? "",
|
|
description: descriptionView.text ?? "",
|
|
dueDate: dueSwitch.isOn ? utcTimestamp(forLocalDate: duePicker.date) : nil
|
|
)
|
|
guard !Task.isCancelled else { return }
|
|
saved(id)
|
|
} catch {
|
|
if !Task.isCancelled {
|
|
show(error: error)
|
|
restoreSaveButton()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func restoreSaveButton() {
|
|
spinner.stopAnimating()
|
|
navigationItem.leftBarButtonItem?.isEnabled = true
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
|
title: "Save",
|
|
style: .done,
|
|
target: self,
|
|
action: #selector(save)
|
|
)
|
|
titleChanged()
|
|
}
|
|
|
|
@objc private func cancel() { dismiss(animated: true) }
|
|
}
|