如何在 Swift 中分割文件名和文件扩展名?

给定捆绑包中文件的名称,我希望将该文件加载到我的 Swift 应用程序中。所以我需要用这个方法:

let soundURL = NSBundle.mainBundle().URLForResource(fname, withExtension: ext)

由于某种原因,该方法需要与文件扩展名分开的文件名。好吧,在大多数语言中把两者分开很容易。但到目前为止,我觉得斯威夫特并不是这样。

这就是我所知道的:

var rt: String.Index = fileName.rangeOfString(".", options:NSStringCompareOptions.BackwardsSearch)
var fname: String = fileName .substringToIndex(rt)
var ext = fileName.substringFromIndex(rt)

如果第一行没有包含类型,那么后面两行就会出现错误。通过它,我在第一行得到了一个错误:

Cannot convert the expression's type '(UnicodeScalarLiteralConvertible, options: NSStringCompareOptions)' to type 'UnicodeScalarLiteralConvertible'

如何将文件名从扩展名中分离出来? 是否有一些优雅的方法来做到这一点?

我对 Swift 非常兴奋,因为它看起来比 Objective C 更优雅。但是现在我发现它有它自己的笨重。


第二次尝试: 我决定创建自己的字符串搜索方法:

func rfind(haystack: String, needle: Character) -> Int {
var a = Array(haystack)


for var i = a.count - 1; i >= 0; i-- {
println(a[i])
if a[i] == needle {
println(i)
return i;
}
}
return -1
}

但是现在我在 var rt: String.Index = rfind(fileName, needle: ".")行得到了一个错误:

'Int' is not convertible to 'String.Index'

如果没有强制转换,我将在后面的两行中得到一个错误。

有人能帮我分割这个文件名和扩展名吗?

95475 次浏览

Strings in Swift can definitely by tricky. If you want a pure Swift method, here's how I would do it:

  1. Use find to find the last occurrence of a "." in the reverse of the string
  2. Use advance to get the correct index of the "." in the original string
  3. Use String's subscript function that takes an IntervalType to get the strings
  4. Package this all up in a function that returns an optional tuple of the name and extension

Something like this:

func splitFilename(str: String) -> (name: String, ext: String)? {
if let rDotIdx = find(reverse(str), ".") {
let dotIdx = advance(str.endIndex, -rDotIdx)
let fname = str[str.startIndex..<advance(dotIdx, -1)]
let ext = str[dotIdx..<str.endIndex]
return (fname, ext)
}
return nil
}

Which would be used like:

let str = "/Users/me/Documents/Something.something/text.txt"
if let split = splitFilename(str) {
println(split.name)
println(split.ext)
}

Which outputs:

/Users/me/Documents/Something.something/text
txt

Or, just use the already available NSString methods like pathExtension and stringByDeletingPathExtension.

Swift 5.0 update:

As pointed out in the comment, you can use this.

let filename: NSString = "bottom_bar.png"
let pathExtention = filename.pathExtension
let pathPrefix = filename.deletingPathExtension

A better way (or at least an alternative in Swift 2.0) is to use the String pathComponents property. This splits the pathname into an array of strings. e.g

if let pathComponents = filePath.pathComponents {
if let last = pathComponents.last {
print(" The last component is \(last)") // This would be the extension
// Getting the last but one component is a bit harder
// Note the edge case of a string with no delimiters!
}
}
// Otherwise you're out of luck, this wasn't a path name!

This is with Swift 2, Xcode 7: If you have the filename with the extension already on it, then you can pass the full filename in as the first parameter and a blank string as the second parameter:

let soundURL = NSBundle.mainBundle()
.URLForResource("soundfile.ext", withExtension: "")

Alternatively nil as the extension parameter also works.

If you have a URL, and you want to get the name of the file itself for some reason, then you can do this:

soundURL.URLByDeletingPathExtension?.lastPathComponent

Swift 4

let soundURL = NSBundle.mainBundle().URLForResource("soundfile.ext", withExtension: "")
soundURL.deletingPathExtension().lastPathComponent

In Swift you can change to NSString to get extension faster:

extension String {
func getPathExtension() -> String {
return (self as NSString).pathExtension
}
}

In Swift 2.1 String.pathExtension is not available anymore. Instead you need to determine it through NSURL conversion:

NSURL(fileURLWithPath: filePath).pathExtension

In Swift 2.1, it seems that the current way to do this is:

let filename = fileURL.URLByDeletingPathExtension?.lastPathComponent
let extension = fileURL.pathExtension

They got rid of pathExtension for whatever reason.

let str = "Hello/this/is/a/filepath/file.ext"
let l = str.componentsSeparatedByString("/")
let file = l.last?.componentsSeparatedByString(".")[0]
let ext = l.last?.componentsSeparatedByString(".")[1]

Works in Swift 5. Adding these behaviors to String class:

extension String {


func fileName() -> String {
return URL(fileURLWithPath: self).deletingPathExtension().lastPathComponent
}


func fileExtension() -> String {
return URL(fileURLWithPath: self).pathExtension
}
}

Example:

let file = "image.png"
let fileNameWithoutExtension = file.fileName()
let fileExtension = file.fileExtension()

Swift 3.0

 let sourcePath = NSURL(string: fnName)?.pathExtension
let pathPrefix = fnName.replacingOccurrences(of: "." + sourcePath!, with: "")

Solution Swift 4

This solution will work for all instances and does not depend on manually parsing the string.

let path = "/Some/Random/Path/To/This.Strange.File.txt"


let fileName = URL(fileURLWithPath: path).deletingPathExtension().lastPathComponent


Swift.print(fileName)

The resulting output will be

This.Strange.File

SWIFT 3.x Shortest Native Solution

let fileName:NSString = "the_file_name.mp3"
let onlyName = fileName.deletingPathExtension
let onlyExt = fileName.pathExtension

No extension or any extra stuff (I've tested. based on @gabbler solution for Swift 2)

Swift 3.x extended solution:

extension String {
func lastPathComponent(withExtension: Bool = true) -> String {
let lpc = self.nsString.lastPathComponent
return withExtension ? lpc : lpc.nsString.deletingPathExtension
}


var nsString: NSString {
return NSString(string: self)
}
}


let path = "/very/long/path/to/filename_v123.456.plist"
let filename = path.lastPathComponent(withExtension: false)

filename constant now contains "filename_v123.456"

Try this for a simple Swift 4 solution

extension String {
func stripExtension(_ extensionSeperator: Character = ".") -> String {
let selfReversed = self.reversed()
guard let extensionPosition = selfReversed.index(of: extensionSeperator) else {  return self  }
return String(self[..<self.index(before: (extensionPosition.base.samePosition(in: self)!))])
}
}


print("hello.there.world".stripExtension())
// prints "hello.there"

A cleaned up answer for Swift 4 with an extension off of PHAsset:

import Photos


extension PHAsset {
var originalFilename: String? {
if #available(iOS 9.0, *),
let resource = PHAssetResource.assetResources(for: self).first {
return resource.originalFilename
}


return value(forKey: "filename") as? String
}
}

As noted in XCode, the originalFilename is the name of the asset at the time it was created or imported.

Maybe I'm getting too late for this but a solution that worked for me and consider quite simple is using the #file compiler directive. Here is an example where I have a class FixtureManager, defined in FixtureManager.swift inside the /Tests/MyProjectTests/Fixturesdirectory. This works both in Xcode and withswift test`

import Foundation


final class FixtureManager {


static let fixturesDirectory = URL(fileURLWithPath: #file).deletingLastPathComponent()


func loadFixture(in fixturePath: String) throws -> Data {
return try Data(contentsOf: fixtureUrl(for: fixturePath))
}


func fixtureUrl(for fixturePath: String) -> URL {
return FixtureManager.fixturesDirectory.appendingPathComponent(fixturePath)
}


func save<T: Encodable>(object: T, in fixturePath: String) throws {
let data = try JSONEncoder().encode(object)
try data.write(to: fixtureUrl(for: fixturePath))
}


func loadFixture<T: Decodable>(in fixturePath: String, as decodableType: T.Type) throws -> T {
let data = try loadFixture(in: fixturePath)
return try JSONDecoder().decode(decodableType, from: data)
}


}

Creates unique "file name" form url including two previous folders

func createFileNameFromURL (colorUrl: URL) -> String {


var arrayFolders = colorUrl.pathComponents


// -3 because last element from url is "file name" and 2 previous are folders on server
let indx = arrayFolders.count - 3
var fileName = ""


switch indx{
case 0...:
fileName = arrayFolders[indx] + arrayFolders[indx+1] + arrayFolders[indx+2]
case -1:
fileName = arrayFolders[indx+1] + arrayFolders[indx+2]
case -2:
fileName = arrayFolders[indx+2]
default:
break
}




return fileName
}

Latest Swift 4.2 works like this:

extension String {
func fileName() -> String {
return URL(fileURLWithPath: self).deletingPathExtension().lastPathComponent
}


func fileExtension() -> String {
return URL(fileURLWithPath: self).pathExtension
}
}

Swift 5

 URL(string: filePath)?.pathExtension

Swift 5 with code sugar

extension String {
var fileName: String {
URL(fileURLWithPath: self).deletingPathExtension().lastPathComponent
}


var fileExtension: String{
URL(fileURLWithPath: self).pathExtension
}
}

Swift 5

URL.deletingPathExtension().lastPathComponent