@@ -64,9 +64,10 @@ public final class NKLogFileManager: @unchecked Sendable {
6464
6565 /// Configures the shared logger instance.
6666 /// - Parameters:
67- /// - minLevel: The minimum log level to be recorded.
68- public static func configure ( logLevel : NKLogLevel = . normal ) {
69- shared . setConfiguration ( logLevel : logLevel )
67+ /// - logLevel: The minimum log level to be recorded.
68+ /// - logDirectory: The directory that contains the log files. Defaults to `Documents/Logs`.
69+ public static func configure ( logLevel : NKLogLevel = . normal , logDirectory : URL ? = nil ) {
70+ shared . setConfiguration ( logLevel : logLevel , logDirectory : logDirectory )
7071 }
7172
7273 /// Configures filter.
@@ -83,7 +84,7 @@ public final class NKLogFileManager: @unchecked Sendable {
8384 shared . setWhitelist ( whitelist : whitelist )
8485 }
8586
86- /// Creates the "Logs" folder inside the user's Documents directory if it does not already exist.
87+ /// Creates the configured log directory if it does not already exist.
8788 ///
8889 /// This static method delegates to the singleton instance (`shared`) and ensures
8990 /// that the log folder structure is created or re-created when needed.
@@ -92,8 +93,7 @@ public final class NKLogFileManager: @unchecked Sendable {
9293 /// (e.g., by iTunes File Sharing, iCloud Drive sync conflicts, or cleanup tools),
9394 /// and must be re-initialized manually.
9495 ///
95- /// The folder path is:
96- /// `~/Documents/Logs`
96+ /// Unless a custom directory was configured, the folder path is `~/Documents/Logs`.
9797 ///
9898 /// If the folder already exists, the method does nothing. If creation fails, the error is silently ignored.
9999 ///
@@ -102,6 +102,14 @@ public final class NKLogFileManager: @unchecked Sendable {
102102 shared . createLogsFolder ( )
103103 }
104104
105+ /// Waits until all pending file writes have completed.
106+ ///
107+ /// Extensions can call this before returning control to the system to prevent their final
108+ /// messages from being lost if the extension process terminates immediately afterward.
109+ public static func flush ( ) {
110+ shared . flush ( )
111+ }
112+
105113 /// Returns the file URL of the currently active log file.
106114 public func currentLogFileURL ( ) -> URL {
107115 return logDirectory . appendingPathComponent ( logFileName )
@@ -110,7 +118,7 @@ public final class NKLogFileManager: @unchecked Sendable {
110118 // MARK: - Configuration
111119
112120 private let logFileName = " log.txt "
113- private let logDirectory : URL
121+ private var logDirectory : URL
114122 public var logLevel : NKLogLevel
115123 private var blacklist : [ String ] = [ ]
116124 private var whitelist : [ String ] = [ ]
@@ -125,7 +133,7 @@ public final class NKLogFileManager: @unchecked Sendable {
125133
126134 // MARK: - Initialization
127135
128- private init ( logLevel : NKLogLevel = . normal , blacklist : [ String ] ? = nil , whitelist : [ String ] ? = nil ) {
136+ private init ( logLevel : NKLogLevel = . normal , logDirectory : URL ? = nil , blacklist : [ String ] ? = nil , whitelist : [ String ] ? = nil ) {
129137 self . logLevel = logLevel
130138 if let blacklist {
131139 self . blacklist = blacklist
@@ -134,31 +142,29 @@ public final class NKLogFileManager: @unchecked Sendable {
134142 self . whitelist = whitelist
135143 }
136144
137- let documents = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) . first !
138- let logsFolder = documents . appendingPathComponent ( " Logs " , isDirectory : true )
145+ let logsFolder = logDirectory ?? Self . defaultLogDirectory ( )
139146 if ! FileManager . default . fileExists ( atPath : logsFolder . path ) {
140147 try? FileManager . default . createDirectory ( at : logsFolder , withIntermediateDirectories : true )
141148 }
142149 self . logDirectory = logsFolder
143150 self . currentLogDate = Self . currentDateString ( )
144151 }
145152
146- /// Creates the "Logs" folder inside the user's Documents directory if it does not already exist.
153+ /// Creates the configured log directory if it does not already exist.
147154 ///
148155 /// This method performs the following steps:
149- /// - Retrieves the path to the `.documentDirectory` using `FileManager` .
150- /// - Appends a "Logs" subdirectory path .
156+ /// - Uses the custom directory supplied during configuration, when available .
157+ /// - Otherwise uses the "Logs" subdirectory of `.documentDirectory` .
151158 /// - Checks if the folder already exists.
152159 /// - If not, it creates the folder, including any intermediate directories.
153- /// - Finally, it sets the `logDirectory` and initializes the current log date .
160+ /// - Initializes the current log date after ensuring the directory exists .
154161 ///
155162 /// If folder creation fails, the method silently ignores the error.
156163 ///
157164 /// - Note: The `logDirectory` property will point to the created `Logs` folder.
158165 ///
159166 private func createLogsFolder ( ) {
160- let documents = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) . first !
161- let logsFolder = documents . appendingPathComponent ( " Logs " , isDirectory : true )
167+ let logsFolder = logDirectory
162168 if ! FileManager . default . fileExists ( atPath : logsFolder . path ) {
163169 try? FileManager . default . createDirectory ( at : logsFolder , withIntermediateDirectories : true )
164170 }
@@ -168,8 +174,16 @@ public final class NKLogFileManager: @unchecked Sendable {
168174 /// Sets configuration parameters for the logger.
169175 /// - Parameters:
170176 /// - logLevel: The NKLogLevel { disabled .. verbose }
171- private func setConfiguration ( logLevel : NKLogLevel ) {
177+ private func setConfiguration ( logLevel : NKLogLevel , logDirectory : URL ? ) {
178+ flush ( )
172179 self . logLevel = logLevel
180+ self . logDirectory = logDirectory ?? Self . defaultLogDirectory ( )
181+ createLogsFolder ( )
182+ }
183+
184+ private static func defaultLogDirectory ( ) -> URL {
185+ let documents = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) . first !
186+ return documents . appendingPathComponent ( " Logs " , isDirectory : true )
173187 }
174188
175189 /// Sets blacklist for the logger.
@@ -337,17 +351,27 @@ public final class NKLogFileManager: @unchecked Sendable {
337351 private func rotateLog ( for date : String ) {
338352 let currentPath = logDirectory . appendingPathComponent ( logFileName )
339353 let rotatedPath = logDirectory . appendingPathComponent ( " log- \( date ) .txt " )
340-
341- do {
342- if fileManager . fileExists ( atPath : currentPath . path ) {
343- try fileManager . moveItem ( at : currentPath , to : rotatedPath )
354+ var coordinationError : NSError ?
355+ let coordinator = NSFileCoordinator ( filePresenter : nil )
356+ coordinator . coordinate (
357+ writingItemAt : currentPath ,
358+ options : . forMoving ,
359+ writingItemAt : rotatedPath ,
360+ options : . forReplacing ,
361+ error : & coordinationError
362+ ) { coordinatedCurrentPath , coordinatedRotatedPath in
363+ do {
364+ if fileManager . fileExists ( atPath : coordinatedCurrentPath . path ) ,
365+ ! fileManager . fileExists ( atPath : coordinatedRotatedPath . path ) {
366+ try fileManager . moveItem ( at : coordinatedCurrentPath , to : coordinatedRotatedPath )
367+ }
368+
369+ if ! fileManager . fileExists ( atPath : coordinatedCurrentPath . path ) {
370+ try Data ( ) . write ( to : coordinatedCurrentPath )
371+ }
372+ } catch {
373+ print ( " Log rotation failed: \( error ) " )
344374 }
345-
346- // Create a new empty log file for today
347- try Data ( ) . write ( to : currentPath )
348-
349- } catch {
350- print ( " Log rotation failed: \( error ) " )
351375 }
352376 }
353377
@@ -363,22 +387,30 @@ public final class NKLogFileManager: @unchecked Sendable {
363387
364388 guard let data = message . data ( using : . utf8 ) else { return }
365389
366- do {
367- if ! fileManager . fileExists ( atPath : logPath . path ) {
368- fileManager . createFile ( atPath : logPath . path , contents : nil )
369- }
390+ var coordinationError : NSError ?
391+ let coordinator = NSFileCoordinator ( filePresenter : nil )
392+ coordinator . coordinate ( writingItemAt : logPath , options : . forMerging , error : & coordinationError ) { coordinatedURL in
393+ do {
394+ if ! fileManager . fileExists ( atPath : coordinatedURL . path ) {
395+ fileManager . createFile ( atPath : coordinatedURL . path , contents : nil )
396+ }
370397
371- let handle = try FileHandle ( forWritingTo : logPath )
398+ let handle = try FileHandle ( forWritingTo : coordinatedURL )
372399
373- defer { try? handle . close ( ) }
400+ defer { try? handle . close ( ) }
374401
375- try handle . seekToEnd ( )
376- try handle . write ( contentsOf : data )
377- } catch {
378- // Ignore log write failures to avoid crashes when the device has no free space.
402+ try handle . seekToEnd ( )
403+ try handle . write ( contentsOf : data )
404+ } catch {
405+ // Ignore log write failures to avoid crashes when the device has no free space.
406+ }
379407 }
380408 }
381409
410+ private func flush ( ) {
411+ logQueue . sync { }
412+ }
413+
382414 // MARK: - Cached DateFormatters
383415
384416 /// Cached formatter for "yyyy-MM-dd". Uses current calendar, locale, and time zone.
0 commit comments