Liu Song’s Projects


~/Projects/mqtt-ios

git clone https://code.lsong.org/mqtt-ios

Commit

Commit
d65b475bc1044fbd2eaafbf77714b7e12792cd08
Author
Philipp Arndt <[email protected]>
Date
2020-05-22 13:19:10 +0200 +0200
Diffstat
 src/MQTTAnalyzer.xcodeproj/project.pbxproj | 64 
 src/MQTTAnalyzer/AppDelegate.swift | 14 
 src/MQTTAnalyzer/Info.plist | 12 
 src/MQTTAnalyzer/MQTTAnalyzer.entitlements | 6 
 src/MQTTAnalyzer/icloud/CertificateFile.swift | 19 
 src/MQTTAnalyzer/icloud/CloudDataManager.swift | 67 
 src/MQTTAnalyzer/icloud/FileLister.swift | 59 
 src/MQTTAnalyzer/model/HostModel.swift | 5 
 src/MQTTAnalyzer/model/persistence/migration/DataMigration.swift | 4 
 src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTCertificateFiles.swift | 59 
  | 5 
 src/MQTTAnalyzer/views/host/form/auth/CertificateFilePickerView.swift | 84 
 src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateFilePickerView.swift | 49 
 src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateLocationPicker.swift | 31 
 src/MQTTAnalyzer/views/host/form/auth/certificates/FileItemView.swift | 31 
 src/MQTTAnalyzer/views/host/form/auth/certificates/FileListView.swift | 37 
 src/MQTTAnalyzer/views/host/form/auth/certificates/NoFilesHelpView.swift | 33 
 src/MQTTAnalyzer/views/host/form/auth/certificates/PKCS12HelpView.swift | 27 

add support fo iCloud documents (incomplete)


diff --git a/src/MQTTAnalyzer/AppDelegate.swift b/src/MQTTAnalyzer/AppDelegate.swift
index 9232e57c65d77759bafa1106d6e4a43c3a1892e1..f3ea0f4dd3f5f346101a9c07e5c59c4a62f61a99 100644
--- a/src/MQTTAnalyzer/AppDelegate.swift
+++ b/src/MQTTAnalyzer/AppDelegate.swift
@@ -18,18 +18,28 @@ 	
 	var syncEngine: SyncEngine?
 	
 	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+		DataMigration.initMigration(afterMigration: self.afterMigration)
+
 		syncEngine = SyncEngine(objects: [
 				SyncObject<HostSetting>()
 			], databaseScope: .private)
 
+		syncEngine?.pull()
 //  AppDelegate.swift
+
+//  AppDelegate.swift
 //
-//  Created by Philipp Arndt on 2019-06-30.
 //  AppDelegate.swift
-//
+
+		CloudDataManager.sharedInstance.initDocumentsDirectory()
+//  AppDelegate.swift
 
 		// Override point for customization after application launch.
 		return true
+	}
+	
+	func afterMigration() {
+		syncEngine?.pushAll()
 	}
 	
 	func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {




diff --git a/src/MQTTAnalyzer/Info.plist b/src/MQTTAnalyzer/Info.plist
index fd9a9b939ee3a8593764d4c0a29d6f581f0a6a5d..cc0c790dd4e6f6b3dfde542422cb1a30028965ac 100644
--- a/src/MQTTAnalyzer/Info.plist
+++ b/src/MQTTAnalyzer/Info.plist
@@ -22,6 +22,18 @@ 	ITSAppUsesNonExemptEncryption
 	<false/>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
+	<key>NSUbiquitousContainers</key>
+	<dict>
+		<key>iCloud.de.rnd7.MQTTAnalyzer</key>
+		<dict>
+			<key>NSUbiquitousContainerIsDocumentScopePublic</key>
+			<true/>
+			<key>NSUbiquitousContainerName</key>
+			<string>MQTTAnalyzer</string>
+			<key>NSUbiquitousContainerSupportedFolderLevels</key>
+			<string>One</string>
+		</dict>
+	</dict>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>




diff --git a/src/MQTTAnalyzer/MQTTAnalyzer.entitlements b/src/MQTTAnalyzer/MQTTAnalyzer.entitlements
index ed4b5583ee1461a0ea5be205f785bda5eaff35a0..a510f123bc6395c7c48058d5a9e4e8ec7a0db167 100644
--- a/src/MQTTAnalyzer/MQTTAnalyzer.entitlements
+++ b/src/MQTTAnalyzer/MQTTAnalyzer.entitlements
@@ -12,6 +12,12 @@ 	com.apple.developer.icloud-services
 	<array>
 		<string>CloudKit</string>
 <?xml version="1.0" encoding="UTF-8"?>
+	<key>com.apple.developer.icloud-container-identifiers</key>
+	</array>
+	<key>com.apple.developer.ubiquity-container-identifiers</key>
+	<array>
+		<string>iCloud.de.rnd7.MQTTAnalyzer</string>
+<?xml version="1.0" encoding="UTF-8"?>
 	<key>com.apple.developer.ubiquity-kvstore-identifier</key>
 	<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
 </dict>




diff --git a/src/MQTTAnalyzer/icloud/CertificateFile.swift b/src/MQTTAnalyzer/icloud/CertificateFile.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b63fffd2838d21dc60d3684387192fecf62a9304
--- /dev/null
+++ b/src/MQTTAnalyzer/icloud/CertificateFile.swift
@@ -0,0 +1,19 @@
+//
+//  File.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+struct CertificateFile: Identifiable, Comparable {
+	let name: String
+	let id = UUID.init()
+	let location: CertificateLocation
+	
+	static func < (lhs: CertificateFile, rhs: CertificateFile) -> Bool {
+		return lhs.name < rhs.name
+    }
+}




diff --git a/src/MQTTAnalyzer/icloud/CloudDataManager.swift b/src/MQTTAnalyzer/icloud/CloudDataManager.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2cb8093451da2bd3c7a37169b96f29b48ac07af2
--- /dev/null
+++ b/src/MQTTAnalyzer/icloud/CloudDataManager.swift
@@ -0,0 +1,67 @@
+//
+//  CloudDataManager.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-17.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+class CloudDataManager {
+
+    static let sharedInstance = CloudDataManager() // Singleton
+
+    struct DocumentsDirectory {
+        static let localDocumentsURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: .userDomainMask).last!
+        static let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
+    }
+
+	func initDocumentsDirectory() {
+		if !isCloudEnabled() {
+			return
+		}
+		
+		if let url = DocumentsDirectory.iCloudDocumentsURL {
+			if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
+				do {
+					try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
+				}
+				catch {
+					print(error.localizedDescription)
+				}
+			}
+			
+			do {
+				try FileManager.default.startDownloadingUbiquitousItem(at: url)
+			}
+			catch {
+				print(error.localizedDescription)
+			}
+		}
+	}
+	
+    // Return the Document directory (Cloud OR Local)
+    // To do in a background thread
+    func getDocumentDiretoryURL() -> URL {
+        if isCloudEnabled() {
+            return DocumentsDirectory.iCloudDocumentsURL!
+        } else {
+            return DocumentsDirectory.localDocumentsURL
+        }
+    }
+
+    // Return true if iCloud is enabled
+    func isCloudEnabled() -> Bool {
+        if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
+        else { return false }
+    }
+	
+	func getiCloudDocumentDiretoryURL() -> URL? {
+		return DocumentsDirectory.iCloudDocumentsURL
+    }
+
+
+	func getLocalDocumentDiretoryURL() -> URL {
+		return DocumentsDirectory.localDocumentsURL
+    }
+}




diff --git a/src/MQTTAnalyzer/icloud/FileLister.swift b/src/MQTTAnalyzer/icloud/FileLister.swift
new file mode 100644
index 0000000000000000000000000000000000000000..f3145c89c6e268094366ca5257c756e3ff8fc51c
--- /dev/null
+++ b/src/MQTTAnalyzer/icloud/FileLister.swift
@@ -0,0 +1,59 @@
+//
+//  FileLister.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+import Combine
+
+class FileLister: ObservableObject {
+	@Published var files: [CertificateFile] = FileLister.listFiles(on: getDefaultLocation())
+	
+	@Published var certificateLocation = getDefaultLocation() {
+        didSet {
+			refresh()
+        }
+    }
+	
+	func refresh() {
+		files = FileLister.listFiles(on: certificateLocation)
+	}
+	
+	class func getDefaultLocation() -> CertificateLocation {
+		return CloudDataManager.sharedInstance.isCloudEnabled() ? CertificateLocation.cloud : .local
+	}
+	
+	class func getUrl(on location: CertificateLocation) -> URL {
+		if location == .cloud {
+			if let url = CloudDataManager.sharedInstance.getiCloudDocumentDiretoryURL() {
+				return url
+			}
+		}
+
+		return CloudDataManager.sharedInstance.getLocalDocumentDiretoryURL()
+	}
+	
+	class func listFiles(on location: CertificateLocation) -> [CertificateFile] {
+		do {
+			let url = FileLister.getUrl(on: location)
+			
+			CloudDataManager.sharedInstance.initDocumentsDirectory()
+			
+			let directoryContents = try FileManager.default
+				.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
+			
+			let files = directoryContents
+				.map { $0.lastPathComponent }
+				.filter { $0.lowercased().range(of: #".*\.(p12|pfx|crt|key)$"#, options: .regularExpression) != nil}
+				.map { CertificateFile(name: $0, location: location) }
+				.sorted()
+
+			return files
+		} catch {
+			return [CertificateFile(name: error.localizedDescription, location: location)]
+		}
+	}
+}




diff --git a/src/MQTTAnalyzer/model/HostModel.swift b/src/MQTTAnalyzer/model/HostModel.swift
index 16eaa03f3c9b9b9423f3c74a8baad9073740a552..edfc80ee1100c29ec7a96c027d5d2d49ba1c89f7 100644
--- a/src/MQTTAnalyzer/model/HostModel.swift
+++ b/src/MQTTAnalyzer/model/HostModel.swift
@@ -9,6 +9,11 @@
 import Foundation
 import SwiftUI
 
+enum CertificateLocation {
+	case cloud
+	case local
+}
+
 enum HostAuthenticationType {
 	case none
 	case usernamePassword




diff --git a/src/MQTTAnalyzer/model/persistence/migration/DataMigration.swift b/src/MQTTAnalyzer/model/persistence/migration/DataMigration.swift
index 5957d639bc3c81a3f3b0b75b58452004e740baca..1ce62b3176bc623daf60ceea2e3a9183da60327b 100644
--- a/src/MQTTAnalyzer/model/persistence/migration/DataMigration.swift
+++ b/src/MQTTAnalyzer/model/persistence/migration/DataMigration.swift
@@ -12,7 +12,7 @@ import IceCream
 
 class DataMigration {
 	
-	class func initMigration(syncEngine: SyncEngine?) {
+	class func initMigration(afterMigration: @escaping () -> Void) {
 		let configuration = Realm.Configuration(
 			schemaVersion: 20,
 			migrationBlock: { migration, oldSchemaVersion in
@@ -23,8 +23,8 @@ 				DataMigrationMultipleTopics.migrate(oldSchemaVersion, migration)
 				DataMigrationEmptyTopic.migrate(oldSchemaVersion, migration)
 				
 				DispatchQueue.global(qos: .background).async {
-//  DataMigration.swift
 //  MQTTAnalyzer
+import Foundation
 				}
 				
 //				Example on how to rename properties:




diff --git a/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTCertificateFiles.swift b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTCertificateFiles.swift
index f1a2b042598cf443d926fdecff0b4b7cd3386d1c..d93e1a9e9b5a8951930f564fe890beffe286d667 100644
--- a/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTCertificateFiles.swift
+++ b/src/MQTTAnalyzer/mqtt/cocoamqtt/CocoaMQTTCertificateFiles.swift
@@ -27,61 +27,60 @@ 	case noIdentify = "No identity"
 }
 
 private func getClientCertFromP12File(certName: String, certPassword: String) throws -> CFArray? {
-	if let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
-//  CertificateFiles.swift
 //  Created by Philipp Arndt on 2020-04-14.
-		
-		guard let p12Data = NSData(contentsOfFile: filePath) else {
-			throw CertificateError.errorOpenFile
-//  CertificateFiles.swift
 import CocoaMQTT
-		
-//  CertificateFiles.swift
+//  Created by Philipp Arndt on 2020-04-14.
 // Create P12 File by using:
+//
 //  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //
-		
+	}
+//
 //  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //  CertificateFiles.swift
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //  MQTTAnalyzer
-//  CertificateFiles.swift
 //  Copyright © 2020 Philipp Arndt. All rights reserved.
+//  Created by Philipp Arndt on 2020-04-14.
+//
 //  MQTTAnalyzer
-//  Created by Philipp Arndt on 2020-04-14.
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //  Copyright © 2020 Philipp Arndt. All rights reserved.
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 
+//
 //  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 import Foundation
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 import CocoaMQTT
-//  MQTTAnalyzer
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
 // Create P12 File by using:
+		} else {
 //  CertificateFiles.swift
-import CocoaMQTT
+import Foundation
 //  CertificateFiles.swift
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
+import CocoaMQTT
 //  Created by Philipp Arndt on 2020-04-14.
+
-//  Created by Philipp Arndt on 2020-04-14.
 //
+//  MQTTAnalyzer
-		}
+	guard let theArray = items, CFArrayGetCount(theArray) > 0 else {
+
 //  CertificateFiles.swift
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
 //  Created by Philipp Arndt on 2020-04-14.
-//  CertificateFiles.swift
+
-//  Created by Philipp Arndt on 2020-04-14.
+//
 //  MQTTAnalyzer
-			throw CertificateError.noIdentify
-		}
-		
+	let dictionary = (theArray as NSArray).object(at: 0)
+
 //  Created by Philipp Arndt on 2020-04-14.
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//  CertificateFiles.swift
+
 //  Copyright © 2020 Philipp Arndt. All rights reserved.
 	}
 	
-	return nil
+	return [identity] as CFArray
 }




diff --git a/src/MQTTAnalyzer/views/host/form/auth/CertificateAuthenticationView.swift b/src/MQTTAnalyzer/views/host/form/auth/CertificateAuthenticationView.swift
deleted file mode 100644
index a95d191dc2526282908c773fa301d8fe1a6ea0a0..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/views/host/form/auth/CertificateAuthenticationView.swift
+++ /dev/null
@@ -1,79 +0,0 @@
-//
-//  AuthPicker.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2020-02-25.
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//
-
-import Foundation
-
-import SwiftUI
-
-struct CertificateAuthenticationView: View {
-
-	@Binding var host: HostFormModel
-	@Binding var clientImpl: HostClientImplType
-	
-	@State var serverCA: String = ""
-	@State var clientCertificate: String = ""
-	@State var clientKey: String = ""
-	@State var clientKeyPassword: String = ""
-	
-	var body: some View {
-		Group {
-			List {
-				if clientImpl == .cocoamqtt {
-					CertificateFileItemView(name: "Client PKCS12", filename: $host.certClient)
-				}
-				else {
-					CertificateFileItemView(name: "Server CA", filename: $host.certServerCA)
-					CertificateFileItemView(name: "Client Certificate", filename: $host.certClient)
-					CertificateFileItemView(name: "Client Key", filename: $host.certClientKey)
-				}
-			}
-			
-			HStack {
-				Text("Password")
-					.font(.headline)
-				
-					Spacer()
-				
-				SecureField(clientImpl == .cocoamqtt ? "password" : "optional key file password", text: $host.certClientKeyPassword)
-					.disableAutocorrection(true)
-					.autocapitalization(.none)
-					.multilineTextAlignment(.trailing)
-					.font(.body)
-			}
-			
-			InfoBox(text: "Certificate files are not synced. "
-				+ "Copy them to all of your devices using Finder / iTunes.")
-		}
-	}
-}
-
-struct CertificateFileItemView: View {
-	let name: String
-	@Binding var filename: String
-	
-	var body: some View {
-		NavigationLink(destination: CertificateFilePickerView(type: name, fileName: $filename)) {
-			HStack {
-				Text(name)
-				.font(.headline)
-				
-				Spacer()
-				
-				Group {
-					if filename.isBlank {
-						Text("select")
-							.foregroundColor(.gray)
-					}
-					else {
-						Text(filename)
-					}
-				}.font(.body)
-			}
-		}
-	}
-}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/CertificateFilePickerView.swift b/src/MQTTAnalyzer/views/host/form/auth/CertificateFilePickerView.swift
deleted file mode 100644
index bd317736a273260b689e446fed36d016c3ee8d94..0000000000000000000000000000000000000000
--- a/src/MQTTAnalyzer/views/host/form/auth/CertificateFilePickerView.swift
+++ /dev/null
@@ -1,84 +0,0 @@
-//
-//  AuthPicker.swift
-//  MQTTAnalyzer
-//
-//  Created by Philipp Arndt on 2020-02-25.
-//  Copyright © 2020 Philipp Arndt. All rights reserved.
-//
-
-import Foundation
-
-import SwiftUI
-
-struct FileItemView: View {
-	let fileName: String
-	@Binding var selection: String
-	
-	var body: some View {
-		HStack {
-			Image(systemName: self.selection == fileName ? "largecircle.fill.circle" : "circle")
-				.foregroundColor(.blue)
-			
-			Image(systemName: "doc.text.fill")
-				.foregroundColor(.secondary)
-			
-			Text(fileName)
-			
-			Spacer()
-		}
-		.onTapGesture {
-			self.selection = self.fileName
-		}
-	}
-}
-
-struct File: Identifiable, Comparable {
-	let name: String
-	let id = UUID.init()
-	
-	static func < (lhs: File, rhs: File) -> Bool {
-		return lhs.name < rhs.name
-    }
-}
-
-struct CertificateFilePickerView: View {
-
-	let type: String
-	
-	@Binding var fileName: String
-	
-	var body: some View {
-		Group {
-			Spacer()
-			
-			InfoBox(text: "Add new *.p12 / *.pfx or *.crt and *.key files with Finder or iTunes.\n\n"
-			 + "Create p12 file using:\n`openssl pkcs12 -export -in user.crt -inkey user.key -out user.p12`")
-			.padding(.horizontal)
-			
-			List {
-				ForEach(listFiles()) { file in
-					FileItemView(fileName: file.name, selection: self.$fileName).font(.body)
-				}
-			}
-		}
-		.navigationBarTitle("Select \(type)")
-	}
-	
-	func listFiles() -> [File] {
-		let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
-
-		do {
-			let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil)
-			
-			let files = directoryContents
-				.map { $0.lastPathComponent }
-				.filter { $0.lowercased().range(of: #".*\.(p12|pfx|crt|key)"#, options: .regularExpression) != nil}
-				.map { File(name: $0) }
-				.sorted()
-
-			return files
-		} catch {
-			return [File(name: error.localizedDescription)]
-		}
-	}
-}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateAuthenticationView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateAuthenticationView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3e8b8cbc7cd1dda6c13594b99248b862bd337031
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateAuthenticationView.swift
@@ -0,0 +1,76 @@
+//
+//  AuthPicker.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-02-25.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+import SwiftUI
+
+struct CertificateAuthenticationView: View {
+
+	@Binding var host: HostFormModel
+	@Binding var clientImpl: HostClientImplType
+	
+	@State var serverCA: String = ""
+	@State var clientCertificate: String = ""
+	@State var clientKey: String = ""
+	@State var clientKeyPassword: String = ""
+	
+	var body: some View {
+		Group {
+			List {
+				if clientImpl == .cocoamqtt {
+					CertificateFileItemView(name: "Client PKCS#12", filename: $host.certClient)
+				}
+				else {
+					CertificateFileItemView(name: "Server CA", filename: $host.certServerCA)
+					CertificateFileItemView(name: "Client Certificate", filename: $host.certClient)
+					CertificateFileItemView(name: "Client Key", filename: $host.certClientKey)
+				}
+			}
+			
+			HStack {
+				Text("Password")
+					.font(.headline)
+				
+					Spacer()
+				
+				SecureField(clientImpl == .cocoamqtt ? "password" : "optional key file password", text: $host.certClientKeyPassword)
+					.disableAutocorrection(true)
+					.autocapitalization(.none)
+					.multilineTextAlignment(.trailing)
+					.font(.body)
+			}
+		}
+	}
+}
+
+struct CertificateFileItemView: View {
+	let name: String
+	@Binding var filename: String
+	
+	var body: some View {
+		NavigationLink(destination: CertificateFilePickerView(type: name, fileName: $filename)) {
+			HStack {
+				Text(name)
+				.font(.headline)
+				
+				Spacer()
+				
+				Group {
+					if filename.isBlank {
+						Text("select")
+							.foregroundColor(.gray)
+					}
+					else {
+						Text(filename)
+					}
+				}.font(.body)
+			}
+		}
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateFilePickerView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateFilePickerView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c9cbbe4f09bd5e0193645e2d38603baba374e45c
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateFilePickerView.swift
@@ -0,0 +1,49 @@
+//
+//  AuthPicker.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-02-25.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+
+import SwiftUI
+import Combine
+
+struct CertificateFilePickerView: View {
+	let type: String
+	
+	@Binding var fileName: String
+	@ObservedObject var fileLister = FileLister()
+		
+	var body: some View {
+		Group {
+			List {
+				if CloudDataManager.sharedInstance.isCloudEnabled() {
+					CertificateLocationSectionView(type: Binding(
+					get: {
+						return self.fileLister.certificateLocation
+					},
+					set: { (newValue) in
+						return self.fileLister.certificateLocation = newValue
+					}))
+				}
+				
+				FileListView(refreshHandler: fileLister.refresh,
+							 files: fileLister.files,
+							 fileName: $fileName,
+							 certificateLocation: Binding(
+				get: {
+					return self.fileLister.certificateLocation
+				},
+				set: { (newValue) in
+					return self.fileLister.certificateLocation = newValue
+				}))
+	
+				PKCS12HelpView()
+			}
+		}
+		.navigationBarTitle("Select \(type)")
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateLocationPicker.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateLocationPicker.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b7fe9003149ba391f90ec5e36f92acfe76079039
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/CertificateLocationPicker.swift
@@ -0,0 +1,31 @@
+//
+//  AuthPicker.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-02-25.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+struct CertificateLocationSectionView: View {
+	@Binding var type: CertificateLocation
+
+	var body: some View {
+		Section(header: Text("Location")) {
+			CertificateLocationPicker(type: $type)
+		}
+	}
+}
+
+struct CertificateLocationPicker: View {
+	@Binding var type: CertificateLocation
+
+	var body: some View {
+		Picker(selection: $type, label: Text("Location")) {
+			Text("iCloud").tag(CertificateLocation.cloud)
+			Text("Local").tag(CertificateLocation.local)
+		}.pickerStyle(SegmentedPickerStyle())
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/FileItemView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/FileItemView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bf799b5c1105a2e51666c481f5f98c5eb69e3734
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/FileItemView.swift
@@ -0,0 +1,31 @@
+//
+//  FileItemView.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import SwiftUI
+
+struct FileItemView: View {
+	let fileName: String
+	@Binding var selection: String
+	
+	var body: some View {
+		HStack {
+			Image(systemName: self.selection == fileName ? "largecircle.fill.circle" : "circle")
+				.foregroundColor(.blue)
+			
+			Image(systemName: "doc.text.fill")
+				.foregroundColor(.secondary)
+			
+			Text(fileName)
+			
+			Spacer()
+		}
+		.onTapGesture {
+			self.selection = self.fileName
+		}
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/FileListView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/FileListView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a10b88af4ec97c06e06c13888ccae65cb8221191
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/FileListView.swift
@@ -0,0 +1,37 @@
+//
+//  FileListView.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+struct FileListView: View {
+	var refreshHandler: () -> Void
+	var files: [CertificateFile]
+	@Binding var fileName: String
+	@Binding var certificateLocation: CertificateLocation
+	
+	var body: some View {
+		Section(header: Text("Files")) {
+			if files.isEmpty {
+				NoFilesHelpView(certificateLocation: $certificateLocation)
+				.foregroundColor(.secondary)
+			}
+			
+			ForEach(files) { file in
+				FileItemView(fileName: file.name, selection: self.$fileName).font(.body)
+			}
+			
+			Button(action: refreshHandler) {
+				HStack {
+					Image(systemName: "arrow.2.circlepath")
+					Text("Refresh")
+				}
+			}.font(.body)
+		}
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/NoFilesHelpView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/NoFilesHelpView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..4a09fa624d567b6498fcc1639efa27d7d155939b
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/NoFilesHelpView.swift
@@ -0,0 +1,33 @@
+//
+//  NoFilesHelpView.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import SwiftUI
+
+struct NoFilesHelpView: View {
+	@Binding var certificateLocation: CertificateLocation
+	
+	var body: some View {
+		VStack(alignment: .leading) {
+			Text("No certificate files here yet.")
+				.font(.headline)
+			
+			Spacer()
+			
+			if certificateLocation == .cloud {
+				Text("Add new *.p12 / *.pfx or *.crt and *.key files to iCloud drive (MQTTAnalyzer folder)")
+				Spacer()
+				Text("Use local files when you prefer them due to security reasons.")
+			}
+			else {
+				Text("Add new *.p12 / *.pfx or *.crt and *.key files with Finder or iTunes.")
+				Spacer()
+				Text("Use the iCloud drive when you like to sync your certificates between your devices.")
+			}
+		}.foregroundColor(.secondary)
+	}
+}




diff --git a/src/MQTTAnalyzer/views/host/form/auth/certificates/PKCS12HelpView.swift b/src/MQTTAnalyzer/views/host/form/auth/certificates/PKCS12HelpView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ef6a0b03e396ce37f0d5679a59024ef7b0be3951
--- /dev/null
+++ b/src/MQTTAnalyzer/views/host/form/auth/certificates/PKCS12HelpView.swift
@@ -0,0 +1,27 @@
+//
+//  PKCS12HelpView.swift
+//  MQTTAnalyzer
+//
+//  Created by Philipp Arndt on 2020-05-22.
+//  Copyright © 2020 Philipp Arndt. All rights reserved.
+//
+
+import SwiftUI
+
+struct PKCS12HelpView: View {
+	var body: some View {
+		Section(header: Text("Use openssl to create PKCS#12 files:")) {
+			VStack(alignment: .leading) {
+				HStack {
+					Text("openssl pkcs12 -export -in user.crt -inkey user.key -out user.p12")
+						.font(.system(size: 14, design: .monospaced))
+						.foregroundColor(.secondary)
+	
+					Spacer()
+				}
+			}
+			.padding(5)
+			.background(Color.gray.opacity(0.2))
+		}
+	}
+}




diff --git a/src/MQTTAnalyzer.xcodeproj/project.pbxproj b/src/MQTTAnalyzer.xcodeproj/project.pbxproj
index fb9124fb571dc2bbc8b3f43412fef6861a8049c3..aefe449db8dd547548d39e50e38a1841f45eb657 100644
--- a/src/MQTTAnalyzer.xcodeproj/project.pbxproj
+++ b/src/MQTTAnalyzer.xcodeproj/project.pbxproj
@@ -17,6 +17,12 @@ 		2209C86C23B720E7007C1D93 /* HostValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2209C86B23B720E7007C1D93 /* HostValidator.swift */; };
 		221C571C2466847800C0DD02 /* QuestionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571B2466847800C0DD02 /* QuestionBox.swift */; };
 		221C571E2466C9CD00C0DD02 /* AWSIOTPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571D2466C9CD00C0DD02 /* AWSIOTPreset.swift */; };
 		221C57202466CC2800C0DD02 /* AWSIOTPresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221C571F2466CC2800C0DD02 /* AWSIOTPresetTests.swift */; };
+		223AF5D12477D575009810E6 /* FileLister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D02477D575009810E6 /* FileLister.swift */; };
+		223AF5D32477D5CA009810E6 /* FileListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D22477D5CA009810E6 /* FileListView.swift */; };
+		223AF5D52477D5F5009810E6 /* FileItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D42477D5F5009810E6 /* FileItemView.swift */; };
+		223AF5D72477D60A009810E6 /* PKCS12HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D62477D60A009810E6 /* PKCS12HelpView.swift */; };
+		223AF5D92477D620009810E6 /* NoFilesHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5D82477D620009810E6 /* NoFilesHelpView.swift */; };
+		223AF5DB2477D64B009810E6 /* CertificateFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223AF5DA2477D64B009810E6 /* CertificateFile.swift */; };
 		223EF0062387084D002ADF3E /* HostSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EF0052387084D002ADF3E /* HostSetting.swift */; };
 		223EF00823870AA5002ADF3E /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 223EF00723870AA5002ADF3E /* CloudKit.framework */; };
 		2253F8D622C8C007007E35A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2253F8D522C8C007007E35A2 /* AppDelegate.swift */; };
@@ -75,6 +81,7 @@ 		22AE643824126A7500C2C4FE /* DiagramPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AE643724126A7500C2C4FE /* DiagramPathTests.swift */; };
 		22AF3AE72388858B001D9F87 /* NewHostFormDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AF3AE62388858B001D9F87 /* NewHostFormDialog.swift */; };
 		22AF3AE9238885AF001D9F87 /* EditHostFormDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AF3AE8238885AF001D9F87 /* EditHostFormDialog.swift */; };
 		22AF3AEB23891267001D9F87 /* HostSettingExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AF3AEA23891267001D9F87 /* HostSettingExamples.swift */; };
+		22C2856224759FD40000C1E8 /* CertificateLocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C2856124759FD40000C1E8 /* CertificateLocationPicker.swift */; };
 		22C386A322CB84900054C385 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C386A222CB84900054C385 /* RootView.swift */; };
 		22C7F0D32416A16600534880 /* MQTTAnalyzerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C7F0D22416A16600534880 /* MQTTAnalyzerUITests.swift */; };
 		22C9F72F23B7486E00892C4B /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 22C9F72E23B7486E00892C4B /* .swiftlint.yml */; };
@@ -104,6 +111,7 @@ 		22E469E2238149FD00D72BD6 /* StringSizeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E469E1238149FD00D72BD6 /* StringSizeExtension.swift */; };
 		22E469E423814CEF00D72BD6 /* JsonFormatString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E469E323814CEF00D72BD6 /* JsonFormatString.swift */; };
 		22E8971722CFBFED00A4B8A3 /* TimeSeriesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E8971622CFBFED00A4B8A3 /* TimeSeriesModel.swift */; };
 		22F6057B23D4911000E6338B /* DataMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F6057A23D4911000E6338B /* DataMigration.swift */; };
+		22F67AE124716ED50082C79F /* CloudDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F67AE024716ED50082C79F /* CloudDataManager.swift */; };
 		22F8BEE723C24A5800422BFF /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F8BEE623C24A5800422BFF /* StringUtils.swift */; };
 		22F8BEE923C31C3B00422BFF /* MoscapsuleClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F8BEE823C31C3B00422BFF /* MoscapsuleClient.swift */; };
 		22F8BEEC23C7871F00422BFF /* LoginDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F8BEEB23C7871F00422BFF /* LoginDialog.swift */; };
@@ -148,6 +156,12 @@ 		221C571B2466847800C0DD02 /* QuestionBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionBox.swift; sourceTree = ""; };
 		221C571D2466C9CD00C0DD02 /* AWSIOTPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSIOTPreset.swift; sourceTree = "<group>"; };
 		221C571F2466CC2800C0DD02 /* AWSIOTPresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSIOTPresetTests.swift; sourceTree = "<group>"; };
 		222C9BFC6423D4AE9EA3416A /* Pods_MQTTAnalyzerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MQTTAnalyzerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		223AF5D02477D575009810E6 /* FileLister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLister.swift; sourceTree = "<group>"; };
+		223AF5D22477D5CA009810E6 /* FileListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListView.swift; sourceTree = "<group>"; };
+		223AF5D42477D5F5009810E6 /* FileItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileItemView.swift; sourceTree = "<group>"; };
+		223AF5D62477D60A009810E6 /* PKCS12HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12HelpView.swift; sourceTree = "<group>"; };
+		223AF5D82477D620009810E6 /* NoFilesHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoFilesHelpView.swift; sourceTree = "<group>"; };
+		223AF5DA2477D64B009810E6 /* CertificateFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateFile.swift; sourceTree = "<group>"; };
 		223EF0032382F99A002ADF3E /* MQTTAnalyzer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MQTTAnalyzer.entitlements; sourceTree = "<group>"; };
 		223EF0052387084D002ADF3E /* HostSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HostSetting.swift; path = MQTTAnalyzer/model/HostSetting.swift; sourceTree = SOURCE_ROOT; };
 		223EF00723870AA5002ADF3E /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
@@ -213,6 +227,7 @@ 		22AE643724126A7500C2C4FE /* DiagramPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramPathTests.swift; sourceTree = ""; };
 		22AF3AE62388858B001D9F87 /* NewHostFormDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewHostFormDialog.swift; sourceTree = "<group>"; };
 		22AF3AE8238885AF001D9F87 /* EditHostFormDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHostFormDialog.swift; sourceTree = "<group>"; };
 		22AF3AEA23891267001D9F87 /* HostSettingExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSettingExamples.swift; sourceTree = "<group>"; };
+		22C2856124759FD40000C1E8 /* CertificateLocationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateLocationPicker.swift; sourceTree = "<group>"; };
 		22C386A222CB84900054C385 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
 		22C7F0D02416A16600534880 /* MQTTAnalyzerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MQTTAnalyzerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		22C7F0D22416A16600534880 /* MQTTAnalyzerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTAnalyzerUITests.swift; sourceTree = "<group>"; };
@@ -244,6 +259,7 @@ 		22E469E1238149FD00D72BD6 /* StringSizeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringSizeExtension.swift; sourceTree = ""; };
 		22E469E323814CEF00D72BD6 /* JsonFormatString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonFormatString.swift; sourceTree = "<group>"; };
 		22E8971622CFBFED00A4B8A3 /* TimeSeriesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSeriesModel.swift; sourceTree = "<group>"; };
 		22F6057A23D4911000E6338B /* DataMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigration.swift; sourceTree = "<group>"; };
+		22F67AE024716ED50082C79F /* CloudDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudDataManager.swift; sourceTree = "<group>"; };
 		22F8BEE623C24A5800422BFF /* StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = "<group>"; };
 		22F8BEE823C31C3B00422BFF /* MoscapsuleClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoscapsuleClient.swift; sourceTree = "<group>"; };
 		22F8BEEB23C7871F00422BFF /* LoginDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDialog.swift; sourceTree = "<group>"; };
@@ -310,6 +326,30 @@ 			);
 			path = cocoamqtt;
 			sourceTree = "<group>";
 		};
+		223AF5CE2477D521009810E6 /* certificates */ = {
+			isa = PBXGroup;
+			children = (
+				22A386F82409404600DF8F94 /* CertificateAuthenticationView.swift */,
+				22A386FA240941B600DF8F94 /* CertificateFilePickerView.swift */,
+				22C2856124759FD40000C1E8 /* CertificateLocationPicker.swift */,
+				223AF5D22477D5CA009810E6 /* FileListView.swift */,
+				223AF5D42477D5F5009810E6 /* FileItemView.swift */,
+				223AF5D62477D60A009810E6 /* PKCS12HelpView.swift */,
+				223AF5D82477D620009810E6 /* NoFilesHelpView.swift */,
+			);
+			path = certificates;
+			sourceTree = "<group>";
+		};
+		223AF5CF2477D55C009810E6 /* icloud */ = {
+			isa = PBXGroup;
+			children = (
+				22F67AE024716ED50082C79F /* CloudDataManager.swift */,
+				223AF5D02477D575009810E6 /* FileLister.swift */,
+				223AF5DA2477D64B009810E6 /* CertificateFile.swift */,
+			);
+			path = icloud;
+			sourceTree = "<group>";
+		};
 		223EF0042387082E002ADF3E /* persistence */ = {
 			isa = PBXGroup;
 			children = (
@@ -353,6 +393,7 @@ 				22FD7D0722C8D39D0078795F /* model */,
 				228104782381708E00112F24 /* views */,
 				22810479238170A200112F24 /* extensions */,
 				22FD7D0622C8D3760078795F /* mqtt */,
+				223AF5CF2477D55C009810E6 /* icloud */,
 				2253F8D722C8C007007E35A2 /* SceneDelegate.swift */,
 				2253F8D522C8C007007E35A2 /* AppDelegate.swift */,
 				2253F8DE22C8C008007E35A2 /* Assets.xcassets */,
@@ -419,10 +460,9 @@ 		};
 		226A6B692445763A00ACDFC3 /* auth */ = {
 			isa = PBXGroup;
 			children = (
+				223AF5CE2477D521009810E6 /* certificates */,
 				22A386F624093EA200DF8F94 /* UsernamePasswordAuthenticationView.swift */,
 				226A6B66244575DB00ACDFC3 /* AuthFormView.swift */,
-				22A386F82409404600DF8F94 /* CertificateAuthenticationView.swift */,
-				22A386FA240941B600DF8F94 /* CertificateFilePickerView.swift */,
 				2202FFE124059D2A00161AD9 /* AuthenticationTypePicker.swift */,
 			);
 			path = auth;
@@ -920,9 +960,12 @@ 				22C9F74223BB5EF100892C4B /* TopicModel.swift in Sources */,
 				2281047723816FD800112F24 /* QuickFilterTextDebounce.swift in Sources */,
 				22A386FB240941B600DF8F94 /* CertificateFilePickerView.swift in Sources */,
 				22FD7D0222C8D2660078795F /* RootModel.swift in Sources */,
+				22C2856224759FD40000C1E8 /* CertificateLocationPicker.swift in Sources */,
 				228104912381770000112F24 /* ReconnectView.swift in Sources */,
+				223AF5D32477D5CA009810E6 /* FileListView.swift in Sources */,
 				22D50F9722CE4C4300F37EAF /* Multimap.swift in Sources */,
 				22810488238173BB00112F24 /* DataSeriesView.swift in Sources */,
+				223AF5D72477D60A009810E6 /* PKCS12HelpView.swift in Sources */,
 				2281048A2381740000112F24 /* MessageView.swift in Sources */,
 				2253F8D822C8C007007E35A2 /* SceneDelegate.swift in Sources */,
 				22C9F73C23BB5E1300892C4B /* MessagesByTopic.swift in Sources */,
@@ -969,6 +1012,7 @@ 				22AF3AE9238885AF001D9F87 /* EditHostFormDialog.swift in Sources */,
 				22AE64342412636300C2C4FE /* DiagramPath.swift in Sources */,
 				220357232445D6F200A98CD3 /* ClientImplFormView.swift in Sources */,
 				2281047623816FD800112F24 /* QuickFilterView.swift in Sources */,
+				22F67AE124716ED50082C79F /* CloudDataManager.swift in Sources */,
 				226A6B612445757900ACDFC3 /* TopicsFormView.swift in Sources */,
 				22F8BEEC23C7871F00422BFF /* LoginDialog.swift in Sources */,
 				226A6B65244575AF00ACDFC3 /* LimitsFormView.swift in Sources */,
@@ -980,12 +1024,16 @@ 				2291424C23BF78000086C251 /* AboutView.swift in Sources */,
 				22AE64362412637A00C2C4FE /* TimeSeriesValue.swift in Sources */,
 				22C9F73723B79A1300892C4B /* MessageTextView.swift in Sources */,
 				22DF52AE246AE8E000A24EDD /* NamedLink.swift in Sources */,
+				223AF5D92477D620009810E6 /* NoFilesHelpView.swift in Sources */,
+				223AF5D52477D5F5009810E6 /* FileItemView.swift in Sources */,
 				22E469DB23801CA000D72BD6 /* ArrayUtils.swift in Sources */,
 				226A6B632445759600ACDFC3 /* ClientIDFormView.swift in Sources */,
 				226A6B67244575DB00ACDFC3 /* AuthFormView.swift in Sources */,
 				2291424923BF6ECE0086C251 /* KeyboardResponsiveModifier.swift in Sources */,
 				226A6B5624449F5400ACDFC3 /* ProtocolPicker.swift in Sources */,
 				220357252445DD6E00A98CD3 /* DeprecationBox.swift in Sources */,
+				223AF5D12477D575009810E6 /* FileLister.swift in Sources */,
+				223AF5DB2477D64B009810E6 /* CertificateFile.swift in Sources */,
 				22FD7D0022C8D2660078795F /* EditHostFormView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1163,9 +1211,9 @@ 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = MQTTAnalyzer/MQTTAnalyzer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-	objects = {
+
 /* Begin PBXBuildFile section */
-	objects = {
+	};
 				DEAD_CODE_STRIPPING = NO;
 				DEVELOPMENT_ASSET_PATHS = "MQTTAnalyzer/Preview\\ Content";
 				DEVELOPMENT_TEAM = 643R6YSRER;
@@ -1176,7 +1224,7 @@ 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
 
-	};
+		22DF52AE246AE8E000A24EDD /* NamedLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DF52AD246AE8E000A24EDD /* NamedLink.swift */; };
 				PRODUCT_BUNDLE_IDENTIFIER = de.rnd7.MQTTAnalyzer;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
@@ -1191,9 +1239,9 @@ 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = MQTTAnalyzer/MQTTAnalyzer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-	objects = {
+
 /* Begin PBXBuildFile section */
-	objects = {
+	};
 				DEAD_CODE_STRIPPING = NO;
 				DEVELOPMENT_ASSET_PATHS = "MQTTAnalyzer/Preview\\ Content";
 				DEVELOPMENT_TEAM = 643R6YSRER;
@@ -1204,7 +1252,7 @@ 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
 
-	};
+		22DF52AE246AE8E000A24EDD /* NamedLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DF52AD246AE8E000A24EDD /* NamedLink.swift */; };
 				PRODUCT_BUNDLE_IDENTIFIER = de.rnd7.MQTTAnalyzer;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;