Add milestone creation and editing

This commit is contained in:
Georg Bauer
2026-07-31 15:30:45 +02:00
parent da0a3aecb4
commit c4e64b5c0c
13 changed files with 591 additions and 21 deletions

View File

@@ -629,6 +629,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func milestone(owner: String, repository: String, id: Int64, page: UInt32) async throws -> MilestonePage
func milestoneEditor(owner: String, repository: String, id: Int64?) async throws -> MilestoneEditorPage
func milestones(owner: String, repository: String, page: UInt32) async throws -> MilestoneListPage
func pull(owner: String, repository: String, number: Int64, page: UInt32) async throws -> PullPage
@@ -651,6 +653,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func saveIssueComment(owner: String, repository: String, number: Int64, commentId: Int64?, body: String) async throws
func saveMilestone(owner: String, repository: String, id: Int64?, title: String, description: String, dueDate: Int64?) async throws -> Int64
func selectServer(index: UInt32) throws
func servers() -> [ServerRow]
@@ -922,6 +926,22 @@ open func milestone(owner: String, repository: String, id: Int64, page: UInt32)a
)
}
open func milestoneEditor(owner: String, repository: String, id: Int64?)async throws -> MilestoneEditorPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_milestone_editor(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionInt64.lower(id)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
liftFunc: FfiConverterTypeMilestoneEditorPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func milestones(owner: String, repository: String, page: UInt32)async throws -> MilestoneListPage {
return
try await uniffiRustCallAsync(
@@ -1091,6 +1111,22 @@ open func saveIssueComment(owner: String, repository: String, number: Int64, com
)
}
open func saveMilestone(owner: String, repository: String, id: Int64?, title: String, description: String, dueDate: Int64?)async throws -> Int64 {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_save_milestone(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionInt64.lower(id),FfiConverterString.lower(title),FfiConverterString.lower(description),FfiConverterOptionInt64.lower(dueDate)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_i64,
completeFunc: ffi_gotcha_core_rust_future_complete_i64,
freeFunc: ffi_gotcha_core_rust_future_free_i64,
liftFunc: FfiConverterInt64.lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func selectServer(index: UInt32)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
uniffiCallStatus in
uniffi_gotcha_core_fn_method_gotchacore_select_server(
@@ -2378,6 +2414,64 @@ public func FfiConverterTypeLabelRow_lower(_ value: LabelRow) -> RustBuffer {
}
public struct MilestoneEditorPage: Equatable, Hashable {
public var title: String
public var description: String
public var dueDate: Int64?
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, description: String, dueDate: Int64?) {
self.title = title
self.description = description
self.dueDate = dueDate
}
}
#if compiler(>=6)
extension MilestoneEditorPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMilestoneEditorPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MilestoneEditorPage {
return
try MilestoneEditorPage(
title: FfiConverterString.read(from: &buf),
description: FfiConverterString.read(from: &buf),
dueDate: FfiConverterOptionInt64.read(from: &buf)
)
}
public static func write(_ value: MilestoneEditorPage, into buf: inout [UInt8]) {
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.description, into: &buf)
FfiConverterOptionInt64.write(value.dueDate, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneEditorPage_lift(_ buf: RustBuffer) throws -> MilestoneEditorPage {
return try FfiConverterTypeMilestoneEditorPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneEditorPage_lower(_ value: MilestoneEditorPage) -> RustBuffer {
return FfiConverterTypeMilestoneEditorPage.lower(value)
}
public struct MilestoneListPage: Equatable, Hashable {
public var rows: [MilestoneRow]
public var hasMore: Bool
@@ -3878,6 +3972,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_milestone() != 57186) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_milestone_editor() != 52261) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_milestones() != 50431) {
return InitializationResult.apiChecksumMismatch
}
@@ -3911,6 +4008,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_save_issue_comment() != 58596) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_save_milestone() != 33075) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -324,6 +324,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE_EDITOR
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone_editor(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository, uint32_t page
@@ -379,6 +384,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_save_issue(uint64_t ptr, RustBu
uint64_t uniffi_gotcha_core_fn_method_gotchacore_save_issue_comment(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, RustBuffer comment_id, RustBuffer body
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SAVE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SAVE_MILESTONE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_save_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer id, RustBuffer title, RustBuffer description, RustBuffer due_date
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
void uniffi_gotcha_core_fn_method_gotchacore_select_server(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
@@ -765,6 +775,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issues(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_milestone(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONE_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONE_EDITOR
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_milestone_editor(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONES
@@ -831,6 +847,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_save_issue(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SAVE_ISSUE_COMMENT
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_save_issue_comment(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SAVE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SAVE_MILESTONE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_save_milestone(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1AED04075E363FBC0FF55773 /* ContentScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5E10787062D1175125BB4C /* ContentScreens.swift */; };
1EDCCB5DE286C1DA407F00F1 /* MilestoneEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA582099DD57D35C748EFB02 /* MilestoneEditorViewController.swift */; };
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */; };
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC828923A1E11A58AE348A74 /* ListScreens.swift */; };
381A27D70EA30C1A3BA1BBC1 /* CommentEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */; };
@@ -30,6 +31,7 @@
8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditorViewController.swift; sourceTree = "<group>"; };
9C0921C76676A14A024BA417 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
AC828923A1E11A58AE348A74 /* ListScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListScreens.swift; sourceTree = "<group>"; };
CA582099DD57D35C748EFB02 /* MilestoneEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneEditorViewController.swift; sourceTree = "<group>"; };
CE5E10787062D1175125BB4C /* ContentScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScreens.swift; sourceTree = "<group>"; };
DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F61515849F6AACD721FE915C /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
@@ -60,6 +62,7 @@
13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */,
722405F999984C7CBE838711 /* IssueEditorViewController.swift */,
AC828923A1E11A58AE348A74 /* ListScreens.swift */,
CA582099DD57D35C748EFB02 /* MilestoneEditorViewController.swift */,
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */,
F75B3E4FFB9C9992517C4D69 /* Support.swift */,
);
@@ -201,6 +204,7 @@
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */,
D2B9B033E92BF8E7C0BDCDB0 /* IssueEditorViewController.swift in Sources */,
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */,
1EDCCB5DE286C1DA407F00F1 /* MilestoneEditorViewController.swift in Sources */,
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */,
F55A89489B2758D694F3B27D /* Support.swift in Sources */,
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */,

View File

@@ -375,9 +375,28 @@ final class MilestonesViewController: RefreshingTableViewController {
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118
let addButton = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(createMilestone)
)
addButton.accessibilityLabel = "New milestone"
navigationItem.rightBarButtonItem = addButton
loadContent(refreshing: false)
}
@objc private func createMilestone() {
let editor = MilestoneEditorViewController(
context: context,
owner: owner,
repository: repository
) { [weak self] _ in
guard let self else { return }
self.dismiss(animated: true) { self.loadContent(refreshing: false) }
}
present(UINavigationController(rootViewController: editor), animated: true)
}
override func loadContent(refreshing: Bool) {
loadPage(1, refreshing: refreshing)
}
@@ -476,9 +495,30 @@ final class MilestoneViewController: RefreshingTableViewController {
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118
let editButton = UIBarButtonItem(
image: context.symbol("pencil"),
style: .plain,
target: self,
action: #selector(editMilestone)
)
editButton.accessibilityLabel = "Edit milestone"
navigationItem.rightBarButtonItem = editButton
loadContent(refreshing: false)
}
@objc private func editMilestone() {
let editor = MilestoneEditorViewController(
context: context,
owner: owner,
repository: repository,
id: id
) { [weak self] _ in
guard let self else { return }
self.dismiss(animated: true) { self.loadContent(refreshing: false) }
}
present(UINavigationController(rootViewController: editor), animated: true)
}
override func loadContent(refreshing: Bool) {
loadPage(1, refreshing: refreshing)
}

View File

@@ -154,11 +154,12 @@ final class IssueViewController: MarkdownPageViewController {
)
addComment.accessibilityLabel = "Add comment"
let editIssue = UIBarButtonItem(
title: "Edit",
image: context.symbol("pencil"),
style: .plain,
target: self,
action: #selector(editIssue)
)
editIssue.accessibilityLabel = "Edit issue"
navigationItem.rightBarButtonItems = [addComment, editIssue]
loadContent(refreshing: false)
}
@@ -884,15 +885,15 @@ private func commentViews(
var headerViews: [UIView] = [author, UIView()]
if comment.canEdit, let editComment {
var configuration = UIButton.Configuration.plain()
configuration.title = "Edit"
configuration.image = UIImage(systemName: "pencil")
configuration.imagePadding = 5
configuration.contentInsets = .zero
let button = UIButton(
configuration: configuration,
primaryAction: UIAction { _ in editComment(comment) }
)
button.accessibilityLabel = "Edit comment by \(comment.author)"
button.widthAnchor.constraint(equalToConstant: 44).isActive = true
button.heightAnchor.constraint(equalToConstant: 44).isActive = true
headerViews.append(button)
}
let header = UIStackView(arrangedSubviews: headerViews)

View File

@@ -337,17 +337,4 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
@objc private func cancel() { dismiss(animated: true) }
private func localDate(forUTCTimestamp timestamp: Int64) -> Date {
var utc = Calendar(identifier: .gregorian)
utc.timeZone = TimeZone(secondsFromGMT: 0)!
let components = utc.dateComponents([.year, .month, .day], from: Date(timeIntervalSince1970: TimeInterval(timestamp)))
return Calendar.current.date(from: components) ?? Date()
}
private func utcTimestamp(forLocalDate date: Date) -> Int64 {
let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
var utc = Calendar(identifier: .gregorian)
utc.timeZone = TimeZone(secondsFromGMT: 0)!
return Int64((utc.date(from: components) ?? date).timeIntervalSince1970)
}
}

View File

@@ -0,0 +1,198 @@
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) }
}

View File

@@ -1,5 +1,22 @@
import UIKit
func localDate(forUTCTimestamp timestamp: Int64) -> Date {
var utc = Calendar(identifier: .gregorian)
utc.timeZone = TimeZone(secondsFromGMT: 0)!
let components = utc.dateComponents(
[.year, .month, .day],
from: Date(timeIntervalSince1970: TimeInterval(timestamp))
)
return Calendar.current.date(from: components) ?? Date()
}
func utcTimestamp(forLocalDate date: Date) -> Int64 {
let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
var utc = Calendar(identifier: .gregorian)
utc.timeZone = TimeZone(secondsFromGMT: 0)!
return Int64((utc.date(from: components) ?? date).timeIntervalSince1970)
}
func errorAlert(_ message: String) -> UIAlertController {
let alert = UIAlertController(title: "Something went wrong", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))