Browse Source

Consolidating startup code and incrementing build number.

pull/92/head
Abhishek 5 years ago
parent
commit
0404302dc5
  1. 4
      Clocker/Clocker/Clocker-Info.plist
  2. 14
      Clocker/ClockerUITests/AboutUsTests.swift
  3. 17
      Clocker/Onboarding/OnboardingParentViewController.swift
  4. 2
      Clocker/Panel/Data Layer/TimezoneData.swift
  5. 170
      Clocker/Preferences/General/PreferencesViewController.swift
  6. 11
      Clocker/Preferences/StartupManager.swift

4
Clocker/Clocker/Clocker-Info.plist

@ -13,11 +13,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.6.09</string> <string>1.6.10</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>64</string> <string>65</string>
<key>Fabric</key> <key>Fabric</key>
<dict> <dict>
<key>APIKey</key> <key>APIKey</key>

14
Clocker/ClockerUITests/AboutUsTests.swift

@ -121,25 +121,25 @@ extension XCTestCase {
let searchField = app.searchFields["AvailableSearchField"] let searchField = app.searchFields["AvailableSearchField"]
searchField.reset(text: place) searchField.reset(text: place)
let results = app.tables["AvailableTimezoneTableView"].cells.staticTexts.matching(matchPredicate) let results = app.tables["AvailableTimezoneTableView"].cells.staticTexts.matching(matchPredicate)
let waiter = XCTWaiter() let waiter = XCTWaiter()
let isHittable = NSPredicate(format: "exists == true", "") let isHittable = NSPredicate(format: "exists == true", "")
let addExpectation = expectation(for: isHittable, let addExpectation = expectation(for: isHittable,
evaluatedWith: results.firstMatch) { () -> Bool in evaluatedWith: results.firstMatch) { () -> Bool in
print("Handler called") print("Handler called")
return true return true
} }
waiter.wait(for: [addExpectation], timeout: 5) waiter.wait(for: [addExpectation], timeout: 5)
if results.count > 0 { if results.count > 0 {
results.firstMatch.click() results.firstMatch.click()
} }
app.buttons["AddAvailableTimezone"].click() app.buttons["AddAvailableTimezone"].click()
if shouldSleep { if shouldSleep {
sleep(2) sleep(2)
} }

17
Clocker/Onboarding/OnboardingParentViewController.swift

@ -1,7 +1,6 @@
// Copyright © 2015 Abhishek Banthia // Copyright © 2015 Abhishek Banthia
import Cocoa import Cocoa
import ServiceManagement
extension NSStoryboard.SceneIdentifier { extension NSStoryboard.SceneIdentifier {
static let welcomeIdentifier = NSStoryboard.SceneIdentifier("welcomeVC") static let welcomeIdentifier = NSStoryboard.SceneIdentifier("welcomeVC")
@ -26,6 +25,8 @@ class OnboardingParentViewController: NSViewController {
@IBOutlet private var backButton: NSButton! @IBOutlet private var backButton: NSButton!
@IBOutlet private var positiveButton: NSButton! @IBOutlet private var positiveButton: NSButton!
private lazy var startupManager = StartupManager()
private lazy var welcomeVC = (storyboard?.instantiateController(withIdentifier: .welcomeIdentifier) as? WelcomeViewController) private lazy var welcomeVC = (storyboard?.instantiateController(withIdentifier: .welcomeIdentifier) as? WelcomeViewController)
private lazy var permissionsVC = (storyboard?.instantiateController(withIdentifier: .onboardingPermissionsIdentifier) as? OnboardingPermissionsViewController) private lazy var permissionsVC = (storyboard?.instantiateController(withIdentifier: .onboardingPermissionsIdentifier) as? OnboardingPermissionsViewController)
@ -283,18 +284,10 @@ class OnboardingParentViewController: NSViewController {
} }
UserDefaults.standard.set(shouldStart ? 1 : 0, forKey: CLStartAtLogin) UserDefaults.standard.set(shouldStart ? 1 : 0, forKey: CLStartAtLogin)
startupManager.toggleLogin(shouldStart)
if !SMLoginItemSetEnabled("com.abhishek.ClockerHelper" as CFString, shouldStart) { shouldStart ?
Logger.log(object: ["Successful": "NO"], for: "Start Clocker Login") Logger.log(object: [:], for: "Enable Launch at Login while Onboarding") :
} else {
Logger.log(object: ["Successful": "YES"], for: "Start Clocker Login")
}
if shouldStart {
Logger.log(object: [:], for: "Enable Launch at Login while Onboarding")
} else {
Logger.log(object: [:], for: "Disable Launch at Login while Onboarding") Logger.log(object: [:], for: "Disable Launch at Login while Onboarding")
}
} }
func logExitPoint() { func logExitPoint() {

2
Clocker/Panel/Data Layer/TimezoneData.swift

@ -244,7 +244,7 @@ class TimezoneData: NSObject, NSCoding {
let old = NSKeyedUnarchiver.unarchiveObject(with: timezone) let old = NSKeyedUnarchiver.unarchiveObject(with: timezone)
if let oldModel = old as? CLTimezoneData { if let oldModel = old as? CLTimezoneData {
// Convert it to new model and add it // Convert it to new model and add it
print("We're still using old Objective-C models"); print("We're still using old Objective-C models")
let newTimezone = TimezoneData(with: oldModel) let newTimezone = TimezoneData(with: oldModel)
newModels.append(newTimezone) newModels.append(newTimezone)
} else if let newModel = old as? TimezoneData { } else if let newModel = old as? TimezoneData {

170
Clocker/Preferences/General/PreferencesViewController.swift

@ -22,8 +22,9 @@ enum RowType {
} }
struct TimezoneMetadata { struct TimezoneMetadata {
let timezone: Timezone let timezone: NSTimeZone
let tages: Set<String> = Set() let tags: Set<String>
let formattedName: String
} }
class PreferencesViewController: ParentViewController { class PreferencesViewController: ParentViewController {
@ -43,8 +44,8 @@ class PreferencesViewController: ParentViewController {
private lazy var startupManager = StartupManager() private lazy var startupManager = StartupManager()
private var filteredArray: [Any] = [] private var filteredArray: [Any] = []
private var timezoneArray: [String] = [] private var timezoneArray: [TimezoneMetadata] = []
private var timezoneFilteredArray: [String] = [] private var timezoneFilteredArray: [TimezoneMetadata] = []
private var dataTask: URLSessionDataTask? = .none private var dataTask: URLSessionDataTask? = .none
private lazy var notimezoneView: NoTimezoneView? = { private lazy var notimezoneView: NoTimezoneView? = {
@ -118,26 +119,26 @@ class PreferencesViewController: ParentViewController {
reloadSearchResults() reloadSearchResults()
} }
private func calculateArray() { private func calculateArray() {
finalArray = [] finalArray = []
func addTimezonesIfNeeded(_ data: [String]) { func addTimezonesIfNeeded(_ data: [TimezoneMetadata]) {
if !data.isEmpty { if !data.isEmpty {
finalArray.append(.timezoneHeader) finalArray.append(.timezoneHeader)
} }
data.forEach { (_) in data.forEach { _ in
finalArray.append(.timezone) finalArray.append(.timezone)
} }
} }
if searchField.stringValue.isEmpty { if searchField.stringValue.isEmpty {
addTimezonesIfNeeded(timezoneArray) addTimezonesIfNeeded(timezoneArray)
} else { } else {
if !filteredArray.isEmpty { if !filteredArray.isEmpty {
finalArray.append(.cityHeader) finalArray.append(.cityHeader)
} }
filteredArray.forEach { (_) in filteredArray.forEach { _ in
finalArray.append(.city) finalArray.append(.city)
} }
addTimezonesIfNeeded(timezoneFilteredArray) addTimezonesIfNeeded(timezoneFilteredArray)
@ -356,28 +357,28 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate
func numberOfRows(in _: NSTableView) -> Int { func numberOfRows(in _: NSTableView) -> Int {
return numberOfSearchResults() return numberOfSearchResults()
} }
func tableView(_ tableView: NSTableView, isGroupRow row: Int) -> Bool { func tableView(_: NSTableView, isGroupRow row: Int) -> Bool {
let currentRowType = finalArray[row] let currentRowType = finalArray[row]
return return
currentRowType == .timezoneHeader || currentRowType == .timezoneHeader ||
currentRowType == .cityHeader currentRowType == .cityHeader
} }
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { func tableView(_: NSTableView, shouldSelectRow row: Int) -> Bool {
print("Should Select Row") print("Should Select Row")
let currentRowType = finalArray[row] let currentRowType = finalArray[row]
return !(currentRowType == .timezoneHeader || currentRowType == .cityHeader) return !(currentRowType == .timezoneHeader || currentRowType == .cityHeader)
} }
func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { func tableView(_: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
// print("Selection Indexes for Proposed Selection: \(proposedSelectionIndexes.first!)") // print("Selection Indexes for Proposed Selection: \(proposedSelectionIndexes.first!)")
return proposedSelectionIndexes return proposedSelectionIndexes
} }
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? { func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
let currentRowType = finalArray[row] let currentRowType = finalArray[row]
switch currentRowType { switch currentRowType {
case .timezoneHeader, .cityHeader: case .timezoneHeader, .cityHeader:
return headerCell(tableView, currentRowType) return headerCell(tableView, currentRowType)
@ -388,40 +389,39 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate
} }
} }
private func timezoneCell(_ tableView: NSTableView, _ rowType: RowType, _ row: Int) -> NSView? { private func timezoneCell(_ tableView: NSTableView, _: RowType, _ row: Int) -> NSView? {
if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell { if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell {
let datasource = searchField.stringValue.isEmpty ? timezoneArray : timezoneFilteredArray let datasource = searchField.stringValue.isEmpty ? timezoneArray : timezoneFilteredArray
guard !datasource.isEmpty else { guard !datasource.isEmpty else {
return nil return nil
} }
let index = searchField.stringValue.isEmpty ? row - 1 : row let index = searchField.stringValue.isEmpty ? row - 1 : row
message.sourceName.stringValue = datasource[index % datasource.count] message.sourceName.stringValue = datasource[index % datasource.count].formattedName
return message return message
} }
return nil return nil
} }
private func cityCell(_ tableView: NSTableView, _ rowType: RowType, _ row: Int) -> NSView? { private func cityCell(_ tableView: NSTableView, _: RowType, _ row: Int) -> NSView? {
if let cityCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell { if let cityCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell {
guard let timezoneData = filteredArray[row % filteredArray.count] as? TimezoneData else { guard let timezoneData = filteredArray[row % filteredArray.count] as? TimezoneData else {
assertionFailure() assertionFailure()
return nil return nil
} }
cityCell.sourceName.stringValue = timezoneData.formattedAddress ?? "Error" cityCell.sourceName.stringValue = timezoneData.formattedAddress ?? "Error"
return cityCell return cityCell
} }
return nil return nil
} }
private func headerCell(_ tableView: NSTableView, _ headerType: RowType) -> NSView? { private func headerCell(_ tableView: NSTableView, _ headerType: RowType) -> NSView? {
if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "headerCell"), owner: self) as? HeaderTableViewCell { if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "headerCell"), owner: self) as? HeaderTableViewCell {
message.headerField.stringValue = headerType == .timezoneHeader ? "Timezones" : "Places" message.headerField.stringValue = headerType == .timezoneHeader ? "Timezones" : "Places"
return message return message
} }
return nil return nil
} }
@ -595,7 +595,16 @@ extension PreferencesViewController {
private func findLocalSearchResultsForTimezones() { private func findLocalSearchResultsForTimezones() {
timezoneFilteredArray = [] timezoneFilteredArray = []
let lowercasedSearchString = searchField.stringValue.lowercased() let lowercasedSearchString = searchField.stringValue.lowercased()
timezoneFilteredArray = timezoneArray.filter { $0.lowercased().contains(lowercasedSearchString) }
timezoneFilteredArray = timezoneArray.filter { (timezoneMetadata) -> Bool in
let tags = timezoneMetadata.tags
for tag in tags where tag.contains(lowercasedSearchString) {
return true
}
return false
}
print(timezoneFilteredArray)
} }
private func generateSearchURL() -> String { private func generateSearchURL() -> String {
@ -644,7 +653,7 @@ extension PreferencesViewController {
isActivityInProgress = false isActivityInProgress = false
reloadSearchResults() reloadSearchResults()
} }
private func reloadSearchResults() { private func reloadSearchResults() {
print("Reloading Search Results") print("Reloading Search Results")
calculateArray() calculateArray()
@ -697,7 +706,7 @@ extension PreferencesViewController {
searchField.placeholderString = "Fetching data might take some time!" searchField.placeholderString = "Fetching data might take some time!"
placeholderLabel.placeholderString = "Retrieving timezone data" placeholderLabel.placeholderString = "Retrieving timezone data"
availableTimezoneTableView.isHidden = true availableTimezoneTableView.isHidden = true
let tuple = "\(latitude),\(longitude)" let tuple = "\(latitude),\(longitude)"
let timeStamp = Date().timeIntervalSince1970 let timeStamp = Date().timeIntervalSince1970
let urlString = "https://maps.googleapis.com/maps/api/timezone/json?location=\(tuple)&timestamp=\(timeStamp)&key=\(CLGeocodingKey)" let urlString = "https://maps.googleapis.com/maps/api/timezone/json?location=\(tuple)&timestamp=\(timeStamp)&key=\(CLGeocodingKey)"
@ -808,9 +817,42 @@ extension PreferencesViewController {
private func setupTimezoneDatasource() { private func setupTimezoneDatasource() {
timezoneArray = [] timezoneArray = []
timezoneArray.append("UTC")
timezoneArray.append("Anywhere on Earth") let anywhereOnEarth = TimezoneMetadata(timezone: NSTimeZone(abbreviation: "GMT-1200")!,
timezoneArray.append(contentsOf: NSTimeZone.knownTimeZoneNames) tags: ["aoe", "anywhere on earth"],
formattedName: "Anywhere on Earth")
timezoneArray.append(anywhereOnEarth)
for (abbreviation, timezone) in TimeZone.abbreviationDictionary {
var tags: Set<String> = [abbreviation.lowercased(), timezone.lowercased()]
var extraTags: [String] = []
if abbreviation == "IST" {
extraTags = ["india", "indian", "kolkata", "calcutta", "mumbai", "delhi", "hyderabad", "noida"]
}
if abbreviation == "PST" {
extraTags = ["los", "los angeles", "california", "san francisco", "bay area", "pacific standard time"]
}
if abbreviation == "UTC" {
extraTags = ["utc", "universal"]
}
if abbreviation == "EST" {
extraTags = ["florida", "new york"]
}
extraTags.forEach { tag in
tags.insert(tag)
}
let timezoneIdentifier = NSTimeZone(name: timezone)!
let timezoneMetadata = TimezoneMetadata(timezone: timezoneIdentifier, tags: tags, formattedName: timezone)
timezoneArray.append(timezoneMetadata)
}
print(TimeZone.knownTimeZoneIdentifiers.count)
print(timezoneArray.count)
} }
@IBAction func addTimeZone(_: NSButton) { @IBAction func addTimeZone(_: NSButton) {
@ -848,17 +890,17 @@ extension PreferencesViewController {
isActivityInProgress = false isActivityInProgress = false
return return
} }
if searchField.stringValue.isEmpty { if searchField.stringValue.isEmpty {
addTimezoneIfSearchStringIsEmpty() addTimezoneIfSearchStringIsEmpty()
} else { } else {
addTimezoneIfSearchStringIsNotEmpty() addTimezoneIfSearchStringIsNotEmpty()
} }
} }
private func addTimezoneIfSearchStringIsEmpty() { private func addTimezoneIfSearchStringIsEmpty() {
let currentRowType = finalArray[availableTimezoneTableView.selectedRow] let currentRowType = finalArray[availableTimezoneTableView.selectedRow]
switch currentRowType { switch currentRowType {
case .timezoneHeader, .cityHeader: case .timezoneHeader, .cityHeader:
isActivityInProgress = false isActivityInProgress = false
@ -869,10 +911,10 @@ extension PreferencesViewController {
return return
} }
} }
private func addTimezoneIfSearchStringIsNotEmpty() { private func addTimezoneIfSearchStringIsNotEmpty() {
let currentRowType = finalArray[availableTimezoneTableView.selectedRow] let currentRowType = finalArray[availableTimezoneTableView.selectedRow]
switch currentRowType { switch currentRowType {
case .timezoneHeader, .cityHeader: case .timezoneHeader, .cityHeader:
isActivityInProgress = false isActivityInProgress = false
@ -883,21 +925,21 @@ extension PreferencesViewController {
cleanupAfterInstallingCity() cleanupAfterInstallingCity()
} }
} }
private func cleanupAfterInstallingCity() { private func cleanupAfterInstallingCity() {
guard let dataObject = filteredArray[availableTimezoneTableView.selectedRow % filteredArray.count] as? TimezoneData else { guard let dataObject = filteredArray[availableTimezoneTableView.selectedRow % filteredArray.count] as? TimezoneData else {
assertionFailure("Data was unexpectedly nil") assertionFailure("Data was unexpectedly nil")
return return
} }
if messageLabel.stringValue.isEmpty { if messageLabel.stringValue.isEmpty {
searchField.stringValue = CLEmptyString searchField.stringValue = CLEmptyString
guard let latitude = dataObject.latitude, let longitude = dataObject.longitude else { guard let latitude = dataObject.latitude, let longitude = dataObject.longitude else {
assertionFailure("Data was unexpectedly nil") assertionFailure("Data was unexpectedly nil")
return return
} }
getTimezone(for: latitude, and: longitude) getTimezone(for: latitude, and: longitude)
} }
} }
@ -908,17 +950,17 @@ extension PreferencesViewController {
if searchField.stringValue.isEmpty == false { if searchField.stringValue.isEmpty == false {
let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow % timezoneFilteredArray.count] let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow % timezoneFilteredArray.count]
let metaInfo = metadata(for: currentSelection) let metaInfo = metadata(for: currentSelection)
data.timezoneID = metaInfo.0 data.timezoneID = metaInfo.0
data.formattedAddress = metaInfo.1 data.formattedAddress = metaInfo.1.formattedName
} else { } else {
let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow - 1] let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow - 1]
let metaInfo = metadata(for: currentSelection) let metaInfo = metadata(for: currentSelection)
data.timezoneID = metaInfo.0 data.timezoneID = metaInfo.0
data.formattedAddress = metaInfo.1 data.formattedAddress = metaInfo.1.formattedName
} }
data.selectionType = .timezone data.selectionType = .timezone
@ -928,33 +970,26 @@ extension PreferencesViewController {
filteredArray = [] filteredArray = []
timezoneFilteredArray = [] timezoneFilteredArray = []
placeholderLabel.placeholderString = CLEmptyString
searchField.stringValue = CLEmptyString
reloadSearchResults() reloadSearchResults()
refreshTimezoneTableView() refreshTimezoneTableView()
refreshMainTable() refreshMainTable()
timezonePanel.close() timezonePanel.close()
placeholderLabel.placeholderString = CLEmptyString
searchField.stringValue = CLEmptyString
searchField.placeholderString = "Enter a city, state or country name" searchField.placeholderString = "Enter a city, state or country name"
availableTimezoneTableView.isHidden = false availableTimezoneTableView.isHidden = false
isActivityInProgress = false isActivityInProgress = false
} }
private func metadata(for selection: String) -> (String, String) { private func metadata(for selection: TimezoneMetadata) -> (String, TimezoneMetadata) {
if selection == "Anywhere on Earth" { if selection.formattedName == "Anywhere on Earth" {
return ("GMT-1200", selection) return ("GMT-1200", selection)
} else if selection == "UTC" { } else if selection.formattedName == "UTC" {
return ("GMT", selection) return ("GMT", selection)
} else { } else {
return (selection, selection) return (selection.formattedName, selection)
} }
} }
@ -964,7 +999,7 @@ extension PreferencesViewController {
searchField.stringValue = CLEmptyString searchField.stringValue = CLEmptyString
placeholderLabel.placeholderString = CLEmptyString placeholderLabel.placeholderString = CLEmptyString
searchField.placeholderString = "Enter a city, state or country name" searchField.placeholderString = "Enter a city, state or country name"
reloadSearchResults() reloadSearchResults()
timezonePanel.close() timezonePanel.close()
@ -1058,11 +1093,6 @@ extension PreferencesViewController {
} }
} }
@IBAction func filterTimezoneArray(_: Any?) {
let lowercasedSearchString = searchField.stringValue.lowercased()
timezoneFilteredArray = timezoneArray.filter { $0.lowercased().contains(lowercasedSearchString) }
}
@IBAction func filterArray(_: Any?) { @IBAction func filterArray(_: Any?) {
messageLabel.stringValue = CLEmptyString messageLabel.stringValue = CLEmptyString
@ -1088,14 +1118,14 @@ extension PreferencesViewController {
} else { } else {
resetSearchView() resetSearchView()
} }
reloadSearchResults() reloadSearchResults()
} }
} }
extension PreferencesViewController { extension PreferencesViewController {
@IBAction func loginPreferenceChanged(_ sender: NSButton) { @IBAction func loginPreferenceChanged(_ sender: NSButton) {
startupManager.toggleLogin(sender) startupManager.toggleLogin(sender.state == .on)
} }
} }

11
Clocker/Preferences/StartupManager.swift

@ -4,25 +4,24 @@ import Cocoa
import ServiceManagement import ServiceManagement
struct StartupManager { struct StartupManager {
func toggleLogin(_ shouldStartAtLogin: Bool) {
func toggleLogin(_ sender: NSButton) { if !SMLoginItemSetEnabled("com.abhishek.ClockerHelper" as CFString, shouldStartAtLogin) {
if !SMLoginItemSetEnabled("com.abhishek.ClockerHelper" as CFString, sender.state == .on) {
Logger.log(object: ["Successful": "NO"], for: "Start Clocker Login") Logger.log(object: ["Successful": "NO"], for: "Start Clocker Login")
addClockerToLoginItemsManually() addClockerToLoginItemsManually()
} else { } else {
Logger.log(object: ["Successful": "YES"], for: "Start Clocker Login") Logger.log(object: ["Successful": "YES"], for: "Start Clocker Login")
} }
} }
private func addClockerToLoginItemsManually() { private func addClockerToLoginItemsManually() {
NSApplication.shared.activate(ignoringOtherApps: true) NSApplication.shared.activate(ignoringOtherApps: true)
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Clocker is unable to set to start at login. 😅" alert.messageText = "Clocker is unable to set to start at login. 😅"
alert.informativeText = "You can manually set it to start at startup by adding Clocker to your login items." alert.informativeText = "You can manually set it to start at startup by adding Clocker to your login items."
alert.addButton(withTitle: "Add Manually") alert.addButton(withTitle: "Add Manually")
alert.addButton(withTitle: "Cancel") alert.addButton(withTitle: "Cancel")
let response = alert.runModal() let response = alert.runModal()
if response.rawValue == 1000 { if response.rawValue == 1000 {
OperationQueue.main.addOperation { OperationQueue.main.addOperation {

Loading…
Cancel
Save