Early return/golden path in Swift - coding-style

I'm used to write code with early return/golden path in Objective-C. I tried this approach in Swift, and noticed that early return comes at the expense of using the forced unwrapping operator (!) when optionals are involved.
Take a method that calculates the size of a directory. First, the golden path version:
private func calculateSize_GoldenPath(directory:String) -> UInt64 {
let fileManager = NSFileManager.defaultManager()
var error : NSError?
var contents = fileManager.contentsOfDirectoryAtPath(directory, error: &error) as [String]?
if contents == nil {
NSLog("Failed to list directory with error \(error)")
return 0
}
var size : UInt64 = 0
for pathComponent in contents! {
let path = directory.stringByAppendingPathComponent(pathComponent)
let attributes : NSDictionary? = fileManager.attributesOfItemAtPath(path, error: &error)
if (attributes == nil) {
NSLog("Failed to read file size of \(path) with error \(error)")
continue
}
size += attributes!.fileSize()
}
return size;
}
Notice how I'm using the ! operator both for the contents and attributes variables.
I'm assuming that overusing the ! operator kind of defeats the purpose of optionals and the type safety they bring. This is how I feel the above method should be coded in Swift to avoid forced unwrapping:
private func calculateSize_IfLet(directory:String) -> UInt64 {
let fileManager = NSFileManager.defaultManager()
var error : NSError?
if let contents = fileManager.contentsOfDirectoryAtPath(directory, error: &error) as? [String] {
var size : UInt64 = 0
for pathComponent in contents {
let path = directory.stringByAppendingPathComponent(pathComponent)
if let attributes : NSDictionary = fileManager.attributesOfItemAtPath(path, error: &error) {
size += attributes.fileSize()
} else {
NSLog("Failed to read file size of \(path) with error \(error)")
}
}
return size
} else {
NSLog("Failed to list directory with error \(error)")
return 0
}
}
However, by using if let I can't do early return anymore. If some methods don't use early return and some do, then I end up with a project with mixed coding style.
My question is, is there a way to code in golden path style without resorting to forced unwrapping when optionals are involved?

Personally I would use extraction of methods, in this case extract the pathComponent section into a separate method thus avoiding the multiple indent and awkward code that mashes conceptually separate code together.
private func calculateSize_IfLet(directoryPath:String) -> UInt64 {
var size : UInt64 = 0
let fileManager = NSFileManager.defaultManager()
var error : NSError?
if let contents = fileManager.contentsOfDirectoryAtPath(directoryPath, error: &error) as? [String] {
size = self.calculateSizeOfDirectory(directoryPath, contents:contents)
} else {
NSLog("Failed to list directory with error \(error)")
}
return size
}
private func calculateSizeOfDirectory(directoryPath:String, contents:[String]) -> UInt64 {
var size : UInt64 = 0
for pathComponent in contents {
var error : NSError?
let fileManager = NSFileManager.defaultManager()
let path = directoryPath.stringByAppendingPathComponent(pathComponent)
if let attributes : NSDictionary = fileManager.attributesOfItemAtPath(path, error: &error) {
size += attributes.fileSize()
} else {
NSLog("Failed to read file size of \(path) with error \(error)")
}
}
return size
}

Related

Append swift 2.0 and xcode 7 [duplicate]

I already have read Read and write data from text file
I need to append the data (a string) to the end of my text file.
One obvious way to do it is to read the file from disk and append the string to the end of it and write it back, but it is not efficient, especially if you are dealing with large files and doing in often.
So the question is "How to append string to the end of a text file, without reading the file and writing the whole thing back"?
so far I have:
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
var err:NSError?
// until we find a way to append stuff to files
if let current_content_of_file = NSString(contentsOfURL: fileurl, encoding: NSUTF8StringEncoding, error: &err) {
"\(current_content_of_file)\n\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}else {
"\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}
if err != nil{
println("CANNOT LOG: \(err)")
}
Here's an update for PointZeroTwo's answer in Swift 3.0, with one quick note - in the playground testing using a simple filepath works, but in my actual app I needed to build the URL using .documentDirectory (or which ever directory you chose to use for reading and writing - make sure it's consistent throughout your app):
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self + "\n").appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
//test
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("logFile.txt")
try "Test \(Date())".appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
Thanks PointZeroTwo.
You should use NSFileHandle, it can seek to the end of the file
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
let string = "\(NSDate())\n"
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
if NSFileManager.defaultManager().fileExistsAtPath(fileurl.path!) {
var err:NSError?
if let fileHandle = NSFileHandle(forWritingToURL: fileurl, error: &err) {
fileHandle.seekToEndOfFile()
fileHandle.writeData(data)
fileHandle.closeFile()
}
else {
println("Can't open fileHandle \(err)")
}
}
else {
var err:NSError?
if !data.writeToURL(fileurl, options: .DataWritingAtomic, error: &err) {
println("Can't write \(err)")
}
}
A variation over some of the posted answers, with following characteristics:
based on Swift 5
accessible as a static function
appends new entries to the end of the file, if it exists
creates the file, if it doesn't exist
no cast to NS objects (more Swiftly)
fails silently if the text cannot be encoded or the path does not exist
class Logger {
static var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
return documentsDirectory.appendingPathComponent(fileName)
}
static func log(_ message: String) {
guard let logFile = logFile else {
return
}
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + ": " + message + "\n").data(using: String.Encoding.utf8) else { return }
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
}
}
}
Here is a way to update a file in a much more efficient way.
let monkeyLine = "\nAdding a šŸµ to the end of the file via FileHandle"
if let fileUpdater = try? FileHandle(forUpdating: newFileUrl) {
// Function which when called will cause all updates to start from end of the file
fileUpdater.seekToEndOfFile()
// Which lets the caller move editing to any position within the file by supplying an offset
fileUpdater.write(monkeyLine.data(using: .utf8)!)
// Once we convert our new content to data and write it, we close the file and thatā€™s it!
fileUpdater.closeFile()
}
Here's a version for Swift 2, using extension methods on String and NSData.
//: Playground - noun: a place where people can play
import UIKit
extension String {
func appendLineToURL(fileURL: NSURL) throws {
try self.stringByAppendingString("\n").appendToURL(fileURL)
}
func appendToURL(fileURL: NSURL) throws {
let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
try data.appendToURL(fileURL)
}
}
extension NSData {
func appendToURL(fileURL: NSURL) throws {
if let fileHandle = try? NSFileHandle(forWritingToURL: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(self)
}
else {
try writeToURL(fileURL, options: .DataWritingAtomic)
}
}
}
// Test
do {
let url = NSURL(fileURLWithPath: "test.log")
try "Test \(NSDate())".appendLineToURL(url)
let result = try String(contentsOfURL: url)
}
catch {
print("Could not write to file")
}
In order to stay in the spirit of #PointZero Two.
Here an update of his code for Swift 4.1
extension String {
func appendLine(to url: URL) throws {
try self.appending("\n").append(to: url)
}
func append(to url: URL) throws {
let data = self.data(using: String.Encoding.utf8)
try data?.append(to: url)
}
}
extension Data {
func append(to url: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: url) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
} else {
try write(to: url)
}
}
}
Update: I wrote a blog post on this, which you can find here!
Keeping things Swifty, here is an example using a FileWriter protocol with default implementation (Swift 4.1 at the time of this writing):
To use this, have your entity (class, struct, enum) conform to this protocol and call the write function (fyi, it throws!).
Writes to the document directory.
Will append to the text file if the file exists.
Will create a new file if the text file doesn't exist.
Note: this is only for text. You could do something similar to write/append Data.
import Foundation
enum FileWriteError: Error {
case directoryDoesntExist
case convertToDataIssue
}
protocol FileWriter {
var fileName: String { get }
func write(_ text: String) throws
}
extension FileWriter {
var fileName: String { return "File.txt" }
func write(_ text: String) throws {
guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileWriteError.directoryDoesntExist
}
let encoding = String.Encoding.utf8
guard let data = text.data(using: encoding) else {
throw FileWriteError.convertToDataIssue
}
let fileUrl = dir.appendingPathComponent(fileName)
if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try text.write(to: fileUrl, atomically: false, encoding: encoding)
}
}
}
All answers (as of now) recreate the FileHandle for every write operation. This may be fine for most applications, but this is also rather inefficient: A syscall is made, and the filesystem is accessed each time you create the FileHandle.
To avoid creating the filehandle multiple times, use something like:
final class FileHandleBuffer {
let fileHandle: FileHandle
let size: Int
private var buffer: Data
init(fileHandle: FileHandle, size: Int = 1024 * 1024) {
self.fileHandle = fileHandle
self.size = size
self.buffer = Data(capacity: size)
}
deinit { try! flush() }
func flush() throws {
try fileHandle.write(contentsOf: buffer)
buffer = Data(capacity: size)
}
func write(_ data: Data) throws {
buffer.append(data)
if buffer.count > size {
try flush()
}
}
}
// USAGE
// Create the file if it does not yet exist
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
let fileHandle = try FileHandle(forWritingTo: fileURL)
// Seek will make sure to not overwrite the existing content
// Skip the seek to overwrite the file
try fileHandle.seekToEnd()
let buffer = FileHandleBuffer(fileHandle: fileHandle)
for i in 0..<count {
let data = getData() // Your implementation
try buffer.write(data)
print(i)
}

Swift2 - initializer for conditional binding must have Optional type, not 'String'

I am getting this error.I have found similar code.I try to fix but I can't.Here is my code
let path = docDir.stringByAppendingPathComponent(dbName)
let fm = NSFileManager.defaultManager()
if !(fm.fileExistsAtPath(path)) {
if let from = (NSBundle.mainBundle().resourcePath! as NSString).stringByAppendingPathComponent(dbName)
{
var error:NSError?
do {
try fm.copyItemAtPath(from, toPath: path)
} catch let error1 as NSError {
error = error1
print("SQLiteDB - failed to copy writable version of DB!")
print("Error - \(error!.localizedDescription)")
return
}
}
}
The error is on line
if let from = (NSBundle.mainBundle().resourcePath! as NSString).stringByAppendingPathComponent(dbName)
report the error:
Initializer for conditional binding must have Optional type, not 'String'
Somebody help me please . Thank a lot
use like follows.
let from:NSString = NSBundle.mainBundle().resourcePath!.stringByAppendingPathComponent(dbPath)
do {
try fileManager.copyItemAtPath(from as String, toPath: toPath as String)
} catch let error1 as NSError {
error = error1
}
please specify the Variable type
if let imageURL:String = array[indexPath.row].absoluteString {
//Do something
}
It because stringByAppendingPathComponent returns just a String, not optional string String?. So you cannot unwrap this String within if let use just let
func stringByAppendingPathComponent(_ str: String) -> String
let from = (NSBundle.mainBundle().resourcePath! as NSString).stringByAppendingPathComponent(dbName)
var error:NSError?
do {
...

Completion handler giving: Type of expression is ambiguous without more context, after updating to swift 2

I'm using the SODA Client for swift (Created by Socrata), I just updated to XCode 7 and swift 2 and found some troubles. The one I haven't been able to solve is the completion handler case when it finds an error, it's not accepting the line "syncCompletion(.Error (reqError))" that supposedly should get the error and return to main thread.
I've seen many errors with the same description here "Type of expression is ambiguous without more context", but not in completion handlers, I saw one using do - catch that is different. I'm don't know enough of swift to find out the way to change this.
Some answers suppose you should rewrite the code because some types could have change in swift 2, but I wouldn't know where to start rewriting.
Thanks in advance for your help.
var task = session.dataTaskWithRequest(request, completionHandler: { data, response, reqError in
// We sync the callback with the main thread to make UI programming easier
let syncCompletion = { res in NSOperationQueue.mainQueue().addOperationWithBlock { completionHandler (res) } }
// Give up if there was a net error
if reqError != nil {
syncCompletion(.Error (reqError))
return
}
// Try to parse the JSON
// println(NSString (data: data, encoding: NSUTF8StringEncoding))
var jsonError: NSError?
var jsonResult: AnyObject!
do {
jsonResult = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
} catch var error as NSError {
jsonError = error
jsonResult = nil
} catch {
fatalError()
}
if let error = jsonError {
syncCompletion(.Error (error))
return
}
// Interpret the JSON
if let a = jsonResult as? [[String: AnyObject]] {
syncCompletion(.Dataset (a))
}
else if let d = jsonResult as? [String: AnyObject] {
if let e : AnyObject = d["error"] {
if let m : AnyObject = d["message"] {
syncCompletion(.Error (NSError(domain: "SODA", code: 0, userInfo: ["Error": m])))
return
}
}
syncCompletion(.Dataset ([d]))
}
else {
syncCompletion(.Error (NSError(domain: "SODA", code: 0, userInfo: nil)))
}
})
Solved, I replaced:
if reqError != nil
With
if let error = reqError
and:
syncCompletion(.Error (reqError))
with
syncCompletion(.Error (error))

retrieve an optional value swift

I have this function:
func richiamoImmagine()
{
let avatarFile = PFUser.currentUser()!["Avatar"] as! PFFile
avatarFile.getDataInBackgroundWithBlock {
(imageData:NSData?, error:NSError?) -> Void in
if error == nil {
if let finalimage = UIImage(data: imageData!) {
self.avatarImage.image = finalimage
}
}
}
}
which retrive an image from parse but if the user doesn't have any image the function cause a crash and this error appear in the log:
fatal error: unexpectedly found nil while unwrapping an Optional value
I know I have to put something like:
if let "variable" == avatarFile
so if the function retrieve nothing at least it doesn't make my app crash! How can I resolve this error?
Probably the place where the crash happens is in this line:
if let finalimage = UIImage(data: imageData!) {
because you're using the forced unwrapping operator. I would add a quick check to the preceding if statement to check for not nil:
if error == nil && imageData != nil {
or even better use optional binding:
if let imageData = imageData where error == nil {
if let finalimage = UIImage(data: imageData) {
self.avatarImage.image = finalimage
}
}
Update: if the error happens in the first line, then you should (again) use optional binding to protect against undesired crashes:
if let avatarFile = PFUser.currentUser()?["Avatar"] as? PFFile {
}

xcode 7 cannot assign a value of type '[NSHTTPCookie]' to a value of type '[NSHTTPCookie]'

I just updated to Xcode7 and am trying to switch my project to using the Swift 2.0 Syntax when I ran into this error in a file from an open source library I'm using. Here's the relevant code:
public lazy var cookies:[String:NSHTTPCookie] = {
let foundCookies: [NSHTTPCookie]
if let responseHeaders = (self.response as? NSHTTPURLResponse)?.allHeaderFields {
foundCookies = NSHTTPCookie.cookiesWithResponseHeaderFields(responseHeaders, forURL:NSURL(string:"")!) as! [NSHTTPCookie]
} else {
foundCookies = []
}
var result:[String:NSHTTPCookie] = [:]
for cookie in foundCookies {
result[cookie.name] = cookie
}
return result
}()
The error reads: Cannot assign a value of type '[NSHTTPCookie]' to a value of type '[NSHTTPCookie]'
Is there something I'm missing here?
Change your code to this:
public lazy var cookies:[String:NSHTTPCookie] = {
let foundCookies: [NSHTTPCookie]
if let responseHeaders = (self.response as? NSHTTPURLResponse)?.allHeaderFields as? [String:String] {
foundCookies = NSHTTPCookie.cookiesWithResponseHeaderFields(responseHeaders, forURL:NSURL(string:"")!)
} else {
foundCookies = []
}
var result:[String:NSHTTPCookie] = [:]
for cookie in foundCookies {
result[cookie.name] = cookie
}
return result
}()
Changes:
if let responseHeaders ... line - did add as? [String:String], because allHeadersFields return type is [NSObject : AnyObject] and not [String:String] required by cookiesWithResponseHeaderFields...
removed as! [NSHTTPCookie] - it has no sense, because cookiesWithResponseHeaderFields return type is already [NSHTTPCookie]
Just check cookiesWithResponseHeaderFields signature:
class func cookiesWithResponseHeaderFields(headerFields: [String : String],
forURL URL: NSURL) -> [NSHTTPCookie]
Please read How do I ask a good question. At least, you should point out to lines where the problem is, etc.

Resources