Implemented retrieving and displaying account transactions in iOS app
This commit is contained in:
parent
dfa31e1422
commit
c3609cd33a
|
@ -11,6 +11,9 @@
|
|||
36266AD727BC6F72008F3B00 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AD627BC6F72008F3B00 /* ContentView.swift */; };
|
||||
36266AD927BC6F74008F3B00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 36266AD827BC6F74008F3B00 /* Assets.xcassets */; };
|
||||
36266ADC27BC6F74008F3B00 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 36266ADB27BC6F74008F3B00 /* Preview Assets.xcassets */; };
|
||||
36266AE427BC7776008F3B00 /* Presenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE327BC7776008F3B00 /* Presenter.swift */; };
|
||||
36266AE727BC7801008F3B00 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE627BC7801008F3B00 /* ViewExtensions.swift */; };
|
||||
36266AEA27BC85D8008F3B00 /* UrlSessionWebClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE927BC85D8008F3B00 /* UrlSessionWebClient.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -19,6 +22,9 @@
|
|||
36266AD627BC6F72008F3B00 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
36266AD827BC6F74008F3B00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
36266ADB27BC6F74008F3B00 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
36266AE327BC7776008F3B00 /* Presenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presenter.swift; sourceTree = "<group>"; };
|
||||
36266AE627BC7801008F3B00 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
|
||||
36266AE927BC85D8008F3B00 /* UrlSessionWebClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSessionWebClient.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -51,8 +57,11 @@
|
|||
36266AD327BC6F72008F3B00 /* fints4k iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
36266AE827BC85C9008F3B00 /* service */,
|
||||
36266AE527BC77E1008F3B00 /* ui */,
|
||||
36266AD427BC6F72008F3B00 /* fints4k_iOSApp.swift */,
|
||||
36266AD627BC6F72008F3B00 /* ContentView.swift */,
|
||||
36266AE327BC7776008F3B00 /* Presenter.swift */,
|
||||
36266AD827BC6F74008F3B00 /* Assets.xcassets */,
|
||||
36266ADA27BC6F74008F3B00 /* Preview Content */,
|
||||
);
|
||||
|
@ -67,6 +76,22 @@
|
|||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
36266AE527BC77E1008F3B00 /* ui */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
36266AE627BC7801008F3B00 /* ViewExtensions.swift */,
|
||||
);
|
||||
path = ui;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
36266AE827BC85C9008F3B00 /* service */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
36266AE927BC85D8008F3B00 /* UrlSessionWebClient.swift */,
|
||||
);
|
||||
path = service;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -74,6 +99,7 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 36266ADF27BC6F74008F3B00 /* Build configuration list for PBXNativeTarget "fints4k iOS" */;
|
||||
buildPhases = (
|
||||
36266AE227BC7189008F3B00 /* ShellScript */,
|
||||
36266ACD27BC6F72008F3B00 /* Sources */,
|
||||
36266ACE27BC6F72008F3B00 /* Frameworks */,
|
||||
36266ACF27BC6F72008F3B00 /* Resources */,
|
||||
|
@ -132,12 +158,35 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
36266AE227BC7189008F3B00 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\ncd ../../..\n./gradlew fints4k:embedAndSignAppleFrameworkForXcode\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
36266ACD27BC6F72008F3B00 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
36266AE427BC7776008F3B00 /* Presenter.swift in Sources */,
|
||||
36266AD727BC6F72008F3B00 /* ContentView.swift in Sources */,
|
||||
36266AEA27BC85D8008F3B00 /* UrlSessionWebClient.swift in Sources */,
|
||||
36266AE727BC7801008F3B00 /* ViewExtensions.swift in Sources */,
|
||||
36266AD527BC6F72008F3B00 /* fints4k_iOSApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -271,6 +320,10 @@
|
|||
DEVELOPMENT_ASSET_PATHS = "\"fints4k iOS/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 7WVYN7QA7Z;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"../../../fints4k/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
"/../fints4k/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
|
@ -282,6 +335,11 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-framework",
|
||||
fints4k,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "net.codinux.banking.fints.fints4k-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
@ -300,6 +358,10 @@
|
|||
DEVELOPMENT_ASSET_PATHS = "\"fints4k iOS/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 7WVYN7QA7Z;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"../../../fints4k/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
"/../fints4k/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
|
@ -311,6 +373,11 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-framework",
|
||||
fints4k,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "net.codinux.banking.fints.fints4k-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
|
@ -1,13 +1,81 @@
|
|||
|
||||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State var transactions: [AccountTransaction] = []
|
||||
|
||||
private let presenter = Presenter()
|
||||
|
||||
|
||||
var body: some View {
|
||||
Text("Hello, world!")
|
||||
.padding()
|
||||
VStack {
|
||||
Button("Fetch transactions") { self.retrieveTransactions() }
|
||||
.padding(.top, 6)
|
||||
|
||||
List(self.transactions, id: \.self) { transaction in
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(transaction.otherPartyName ?? transaction.bookingText ?? "")
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.frame(height: 20)
|
||||
|
||||
Text(transaction.reference)
|
||||
.styleAsDetail()
|
||||
.padding(.top, 4)
|
||||
.lineLimit(2)
|
||||
.frame(height: 46, alignment: .center)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
|
||||
Text(presenter.formatAmount(transaction.amount))
|
||||
.styleAsDetail()
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(presenter.formatDate(transaction.valueDate))
|
||||
.styleAsDetail()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func retrieveTransactions() {
|
||||
// TODO: set your credentials here
|
||||
self.presenter.retrieveTransactions("", "", "", "", self.handleRetrieveTransactionsResult)
|
||||
}
|
||||
|
||||
private func handleRetrieveTransactionsResult(_ result: AddAccountResponse) {
|
||||
NSLog("Retrieved response: \(result.retrievedTransactionsResponses)")
|
||||
|
||||
if (result.successful) {
|
||||
var allTransactions: [AccountTransaction] = []
|
||||
|
||||
for accountResponse in result.retrievedTransactionsResponses {
|
||||
if let transactions = accountResponse.retrievedData?.bookedTransactions as? Set<AccountTransaction> { // it's a Set
|
||||
allTransactions.append(contentsOf: transactions)
|
||||
}
|
||||
if let transactions = accountResponse.retrievedData?.bookedTransactions as? [AccountTransaction] {
|
||||
allTransactions.append(contentsOf: transactions)
|
||||
}
|
||||
}
|
||||
|
||||
self.transactions = allTransactions
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
class Presenter {
|
||||
|
||||
private let fintsClient = FinTsClientDeprecated(callback: SimpleFinTsClientCallback(), webClient: UrlSessionWebClient())
|
||||
|
||||
private let formatter = DateFormatter()
|
||||
|
||||
|
||||
func retrieveTransactions(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (AddAccountResponse) -> Void) {
|
||||
self.fintsClient.addAccountAsync(parameter: AddAccountParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
|
||||
}
|
||||
|
||||
|
||||
func formatDate(_ date: Kotlinx_datetimeLocalDate) -> String {
|
||||
formatter.dateStyle = .short
|
||||
|
||||
// return self.formatter.string(from: date.toNSDate())
|
||||
|
||||
return date.description()
|
||||
}
|
||||
|
||||
func formatAmount(_ amount: Money) -> String {
|
||||
return amount.description()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
class UrlSessionWebClient : IWebClient {
|
||||
|
||||
func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (WebClientResponse) -> Void) {
|
||||
let request = requestFor(url, "POST", body)
|
||||
|
||||
executeRequestAsync(request, callback)
|
||||
}
|
||||
|
||||
func getAsync(_ url: String, callback: @escaping (WebClientResponse) -> Void) {
|
||||
let request = requestFor(url, "GET")
|
||||
|
||||
executeRequestAsync(request, callback)
|
||||
}
|
||||
|
||||
|
||||
func get(_ url: String) -> WebClientResponse {
|
||||
let request = requestFor(url, "GET")
|
||||
|
||||
return executeRequestSynchronous(request)
|
||||
}
|
||||
|
||||
func getData(_ url: String) -> Data? {
|
||||
let request = requestFor(url, "GET")
|
||||
|
||||
return executeDataRequestSynchronous(request)
|
||||
}
|
||||
|
||||
func head(_ url: String) -> WebClientResponse {
|
||||
let request = requestFor(url, "HEAD")
|
||||
|
||||
return executeRequestSynchronous(request)
|
||||
}
|
||||
|
||||
|
||||
func requestFor(_ url: String, _ method: String, _ body: String? = nil) -> URLRequest {
|
||||
guard let requestUrl = URL(string: url) else { fatalError() }
|
||||
|
||||
var request = URLRequest(url: requestUrl)
|
||||
request.httpMethod = method
|
||||
|
||||
if let body = body {
|
||||
request.httpBody = body.data(using: String.Encoding.utf8)
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
func executeRequestAsync(_ request: URLRequest, _ callback: @escaping (WebClientResponse) -> Void) {
|
||||
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
|
||||
// we have to dispatch response back to main thread as in Kotlin/Native objects can only be changed on one thread -> do all logic on main thread only network access in backbackground thread
|
||||
DispatchQueue.main.async {
|
||||
var webClientResponse = WebClientResponse(successful: false, responseCode: -1, error: KotlinException(message: error?.localizedDescription), body: nil)
|
||||
|
||||
if let data = data, let httpResponse = response as? HTTPURLResponse {
|
||||
webClientResponse = WebClientResponse(successful: true, responseCode: Int32(httpResponse.statusCode), error: nil, body: String(data: data, encoding: .ascii))
|
||||
}
|
||||
|
||||
callback(webClientResponse)
|
||||
}
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
func executeRequestSynchronous(_ request: URLRequest) -> WebClientResponse {
|
||||
var data: Data?, urlResponse: URLResponse?, error: Error?
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let dataTask = URLSession.shared.dataTask(with: request) {
|
||||
data = $0
|
||||
urlResponse = $1
|
||||
error = $2
|
||||
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
return mapResponse(data, urlResponse, error)
|
||||
}
|
||||
|
||||
private func mapResponse(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> WebClientResponse {
|
||||
// let mappedError = error == nil ? nil : KotlinException(message: error?.localizedDescription) // TODO:
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
let statusCode = Int32(httpResponse.statusCode)
|
||||
// let isSuccessful = mappedError == nil // TODO
|
||||
// && statusCode >= 200 && statusCode <= 299
|
||||
let isSuccessful = statusCode >= 200 && statusCode <= 299
|
||||
|
||||
if let data = data {
|
||||
return WebClientResponse(successful: isSuccessful, responseCode: statusCode, error: nil, body: String(data: data, encoding: .ascii))
|
||||
}
|
||||
else {
|
||||
return WebClientResponse(successful: isSuccessful, responseCode: statusCode, error: nil, body: nil)
|
||||
}
|
||||
}
|
||||
else {
|
||||
return WebClientResponse(successful: false, responseCode: -1, error: nil, body: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func executeDataRequestSynchronous(_ request: URLRequest) -> Data? {
|
||||
var data: Data?
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let dataTask = URLSession.shared.dataTask(with: request) { receivedData, _, _ in
|
||||
data = receivedData
|
||||
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
extension Color {
|
||||
static let lightText = Color(UIColor.lightText)
|
||||
static let darkText = Color(UIColor.darkText)
|
||||
|
||||
static let label = Color(UIColor.label)
|
||||
static let secondaryLabel = Color(UIColor.secondaryLabel)
|
||||
static let tertiaryLabel = Color(UIColor.tertiaryLabel)
|
||||
static let quaternaryLabel = Color(UIColor.quaternaryLabel)
|
||||
|
||||
static let link = Color(UIColor.link)
|
||||
|
||||
static let systemBackground = Color(UIColor.systemBackground)
|
||||
static let secondarySystemBackground = Color(UIColor.secondarySystemBackground)
|
||||
static let tertiarySystemBackground = Color(UIColor.tertiarySystemBackground)
|
||||
|
||||
static let systemGroupedBackground = Color(UIColor.systemGroupedBackground)
|
||||
|
||||
// There are more..
|
||||
|
||||
static var destructive: Color {
|
||||
if UIColor.responds(to: Selector(("_systemDestructiveTintColor"))) {
|
||||
if let red = UIColor.perform(Selector(("_systemDestructiveTintColor")))?.takeUnretainedValue() as? UIColor {
|
||||
return Color(red)
|
||||
}
|
||||
}
|
||||
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
|
||||
func makeBackgroundTapable() -> some View {
|
||||
return self.background(Color.systemBackground)
|
||||
}
|
||||
|
||||
|
||||
func detailForegroundColor() -> some View {
|
||||
return self
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
|
||||
func detailFont() -> some View {
|
||||
return self
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
func styleAsDetail() -> some View {
|
||||
return self
|
||||
.detailFont()
|
||||
.detailForegroundColor()
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ import net.dankito.banking.fints.model.*
|
|||
import net.dankito.banking.fints.response.BankResponse
|
||||
import net.dankito.banking.fints.response.client.*
|
||||
import net.dankito.banking.fints.response.segments.*
|
||||
import net.dankito.banking.fints.webclient.IWebClient
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
|
||||
|
@ -29,6 +30,11 @@ open class FinTsClientDeprecated @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
|
||||
constructor(callback: FinTsClientCallback) : this(callback, FinTsJobExecutor()) // Swift does not support default parameter values -> create constructor overloads
|
||||
|
||||
constructor(callback: FinTsClientCallback, webClient: IWebClient) : this(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves information about bank (e.g. supported HBCI versions, FinTS server address,
|
||||
* supported jobs, ...).
|
||||
|
|
|
@ -10,6 +10,11 @@ open class SimpleFinTsClientCallback(
|
|||
protected val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null
|
||||
) : FinTsClientCallback {
|
||||
|
||||
constructor() : this(null) // Swift does not support default parameter values -> create constructor overloads
|
||||
|
||||
constructor(enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)?) : this(enterTan, null)
|
||||
|
||||
|
||||
override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>,
|
||||
suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ expect class DateFormatter constructor(pattern: String) {
|
|||
|
||||
fun format(date: LocalDateTime): String
|
||||
|
||||
fun format(date: LocalDate): String
|
||||
|
||||
fun parseDate(dateString: String): LocalDate?
|
||||
|
||||
fun parse(dateString: String): LocalDateTime?
|
||||
|
|
|
@ -55,7 +55,11 @@ val LocalDate.millisSinceEpochAtEuropeBerlin: Long
|
|||
get() = this.toEpochMillisecondsAt(TimeZone.europeBerlin)
|
||||
|
||||
fun LocalDate.toEpochMillisecondsAt(timeZone: TimeZone): Long {
|
||||
return this.atTime(0, 0).toInstant(timeZone).toEpochMilliseconds()
|
||||
return this.toLocalDateTime().toInstant(timeZone).toEpochMilliseconds()
|
||||
}
|
||||
|
||||
fun LocalDate.toLocalDateTime(): LocalDateTime {
|
||||
return this.atTime(0, 0)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package net.dankito.utils.multiplatform
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import net.dankito.utils.multiplatform.extensions.toLocalDateTime
|
||||
import net.dankito.utils.multiplatform.extensions.toNSDate
|
||||
import platform.Foundation.*
|
||||
|
||||
|
||||
|
@ -34,9 +36,12 @@ actual class DateFormatter actual constructor(val pattern: String): NSDateFormat
|
|||
}
|
||||
|
||||
|
||||
actual fun format(date: LocalDate): String {
|
||||
return format(date.toLocalDateTime())
|
||||
}
|
||||
|
||||
actual fun format(date: LocalDateTime): String {
|
||||
val instant = date.toInstant(TimeZone.currentSystemDefault())
|
||||
val nsDate = instant.toNSDate()
|
||||
val nsDate = date.toNSDate()
|
||||
|
||||
return this.stringFromDate(nsDate)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2019-2020 JetBrains s.r.o.
|
||||
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package net.dankito.utils.multiplatform.extensions
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.*
|
||||
|
||||
/**
|
||||
* Converts the [Instant] to an instance of [NSDate].
|
||||
*
|
||||
* The conversion is lossy: Darwin uses millisecond precision to represent dates, and [Instant] allows for nanosecond
|
||||
* resolution.
|
||||
*/
|
||||
fun Instant.toNSDate(): NSDate {
|
||||
val secs = epochSeconds * 1.0 + nanosecondsOfSecond / 1.0e9
|
||||
if (secs < NSDate.distantPast.timeIntervalSince1970 || secs > NSDate.distantFuture.timeIntervalSince1970) {
|
||||
throw IllegalArgumentException("Boundaries of NSDate exceeded")
|
||||
}
|
||||
return NSDate.dateWithTimeIntervalSince1970(secs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the [NSDate] to the corresponding [Instant].
|
||||
*
|
||||
* Even though Darwin only uses millisecond precision, it is possible that [date] uses larger resolution, storing
|
||||
* microseconds or even nanoseconds. In this case, the sub-millisecond parts of [date] are rounded to the nearest
|
||||
* millisecond, given that they are likely to be conversion artifacts.
|
||||
*/
|
||||
fun NSDate.toKotlinInstant(): Instant {
|
||||
val secs = timeIntervalSince1970()
|
||||
val millis = secs * 1000 + if (secs > 0) 0.5 else -0.5
|
||||
return Instant.fromEpochMilliseconds(millis.toLong())
|
||||
}
|
||||
|
||||
|
||||
fun LocalDateTime.toNSDate(): NSDate {
|
||||
val instant = this.toInstant(TimeZone.currentSystemDefault())
|
||||
|
||||
return instant.toNSDate()
|
||||
}
|
||||
|
||||
fun LocalDate.toNSDate(): NSDate {
|
||||
return this.toLocalDateTime().toNSDate()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the [TimeZone] to [NSTimeZone].
|
||||
*
|
||||
* If the time zone is represented as a fixed number of seconds from UTC+0 (for example, if it is the result of a call
|
||||
* to [TimeZone.offset]) and the offset is not given in even minutes but also includes seconds, this method throws
|
||||
* [DateTimeException] to denote that lossy conversion would happen, as Darwin internally rounds the offsets to the
|
||||
* nearest minute.
|
||||
*/
|
||||
fun TimeZone.toNSTimeZone(): NSTimeZone = if (this is ZoneOffset) {
|
||||
require (totalSeconds % 60 == 0) {
|
||||
"Lossy conversion: Darwin uses minute precision for fixed-offset time zones"
|
||||
}
|
||||
NSTimeZone.timeZoneForSecondsFromGMT(totalSeconds.convert())
|
||||
} else {
|
||||
NSTimeZone.timeZoneWithName(id) ?: NSTimeZone.timeZoneWithAbbreviation(id)!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the [NSTimeZone] to the corresponding [TimeZone].
|
||||
*/
|
||||
fun NSTimeZone.toKotlinTimeZone(): TimeZone = TimeZone.of(name)
|
||||
|
||||
/**
|
||||
* Converts the given [LocalDate] to [NSDateComponents].
|
||||
*
|
||||
* Of all the fields, only the bare minimum required for uniquely identifying the date are set.
|
||||
*/
|
||||
fun LocalDate.toNSDateComponents(): NSDateComponents {
|
||||
val components = NSDateComponents()
|
||||
components.year = year.convert()
|
||||
components.month = monthNumber.convert()
|
||||
components.day = dayOfMonth.convert()
|
||||
return components
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given [LocalDate] to [NSDateComponents].
|
||||
*
|
||||
* Of all the fields, only the bare minimum required for uniquely identifying the date and time are set.
|
||||
*/
|
||||
public fun LocalDateTime.toNSDateComponents(): NSDateComponents {
|
||||
val components = date.toNSDateComponents()
|
||||
components.hour = hour.convert()
|
||||
components.minute = minute.convert()
|
||||
components.second = second.convert()
|
||||
components.nanosecond = nanosecond.convert()
|
||||
return components
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package net.dankito.utils.multiplatform
|
|||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import net.dankito.utils.multiplatform.extensions.toLocalDateTime
|
||||
|
||||
actual class DateFormatter actual constructor(pattern: String) {
|
||||
|
||||
|
@ -10,6 +11,10 @@ actual class DateFormatter actual constructor(pattern: String) {
|
|||
actual constructor(dateStyle: DateFormatStyle, timeStyle: DateFormatStyle) : this("")
|
||||
|
||||
|
||||
actual fun format(date: LocalDate): String {
|
||||
return format(date.toLocalDateTime())
|
||||
}
|
||||
|
||||
// TODO: implement for Logger, get current time formatted as string
|
||||
actual fun format(date: LocalDateTime): String {
|
||||
return "" // is only used in rare cases, don't implement right now
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package net.dankito.utils.multiplatform
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import net.dankito.utils.multiplatform.extensions.toLocalDateTime
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
@ -28,6 +29,10 @@ actual class DateFormatter actual constructor(pattern: String) {
|
|||
: this((DateFormat.getDateTimeInstance(dateStyle.convert(), timeStyle.convert()) as? SimpleDateFormat)?.toPattern() ?: "")
|
||||
|
||||
|
||||
actual fun format(date: LocalDate): String {
|
||||
return format(date.toLocalDateTime())
|
||||
}
|
||||
|
||||
actual fun format(date: LocalDateTime): String {
|
||||
return formatter.format(date.toJavaLocalDateTime())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package net.dankito.utils.multiplatform
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import net.dankito.utils.multiplatform.extensions.toLocalDateTime
|
||||
|
||||
|
||||
actual class DateFormatter actual constructor(pattern: String) {
|
||||
|
@ -10,6 +11,10 @@ actual class DateFormatter actual constructor(pattern: String) {
|
|||
actual constructor(dateStyle: DateFormatStyle, timeStyle: DateFormatStyle) : this("")
|
||||
|
||||
|
||||
actual fun format(date: LocalDate): String {
|
||||
return format(date.toLocalDateTime())
|
||||
}
|
||||
|
||||
// TODO: implement for Logger, get current time formatted as string
|
||||
actual fun format(date: LocalDateTime): String {
|
||||
return "" // is only used in rare cases, don't implement right now
|
||||
|
|
Loading…
Reference in New Issue