Using Swift with Cocoa and Objective-C (Swift 2.1)

Adopting Cocoa Design Patterns


https://developer.apple.com/library/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID6

One aid in writing well-designed,resilient apps is to use Cocoa’s established design patterns. Many of these patterns rely on classes defined in Objective-C. Because of Swift’s interoperability with Objective-C,you can take advantage of these common patterns in your Swift code. In many cases,you can use Swift Language features to extend or simplify existing Cocoa patterns,making them more powerful and easier to use.

Delegation

In both Swift and Objective-C,delegation is often expressed with a protocol that defines the interaction and a conforming delegate property. Just as in Objective-C,before you send a message that a delegate may not respond to,you ask the delegate whether it responds to the selector. In Swift,you can use optional chaining to invoke an optional protocol method on a possiblynilobject and unwrap the possible result usingif–letSyntax. The code listing below illustrates the following process:

  1. Check thatmyDelegateis notnil.

  2. Check thatmyDelegateimplements the methodwindow:willUseFullScreenContentSize:.

  3. If 1 and 2 hold true,invoke the method and assign the result of the method to the value namedfullScreenSize.

  4. Print the return value of the method.

  1. class MyDelegate: NSObject, NSWindowDelegate {
  2. func window(window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
  3. return proposedSize
  4. }
  5. var myDelegate: NSWindowDelegate? = MyDelegate()
  6. if let fullScreenSize = myDelegate?.window?(myWindow,116)"> willUseFullScreenContentSize: mySize) {
  7. print(NsstringFromSize(fullScreenSize))
  8. }
Lazy Initialization

Alazy propertyis a property whose underlying value is only initialized when the property is first accessed. Lazy properties are useful when the initial value for a property either requires complex or computationally expensive setup,or cannot be determined until after an instance’s initialization is complete.

In Objective-C,a property may override its synthesized getter method such that the underlying instance variable is conditionally initialized if its value isnil:

  1. @property NSXMLDocument *XMLDocument;
  2. - (NSXMLDocument *)XMLDocument {
  3. if (_XMLDocument == nil) {
  4. _XMLDocument = [[NSXMLDocument alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
  5. }
  6. return _XMLDocument;
  7. }

In Swift,a stored property with an initial value can be declared with thelazymodifier to have the expression calculating the initial value only evaluated when the property is first accessed:

  1. lazy var XMLDocument: NSXMLDocument = try! NSXMLDocument(contentsOfURL: NSBundle.mainBundle().URLForResource("document",116)"> withExtension: "xml")!,116)"> options: 0)

Because a lazy property is only computed when accessed for a fully-initialized instance it may access constant or variable properties in its default value initialization expression:

pattern: String
  • regex: NSRegularExpression = NSRegularExpression(pattern: self.pattern,116)"> options: [])
  • For values that require additional setup beyond initialization,you can assign the default value of the property to a self-evaluating closure that returns a fully-initialized value:

    ISO8601DateFormatter: NSDateFormatter = {
  • let formatter = NSDateFormatter()
  • formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
  • dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
  • formatter
  • }()
  • NOTE

    If a lazy property has not yet been initialized and is accessed by more than one thread at the same time,there is no guarantee that the property will be initialized only once.

    For more information,seeLazy Stored PropertiesinThe Swift Programming Language (Swift 2.1).

    Error Handling

    In Cocoa,methods that produce errors take anNSErrorpointer parameter as their last parameter,which populates its argument with anNSErrorobject if an error occurs. Swift automatically translates Objective-C methods that produce errors into methods that throw an error according to Swift’s native error handling functionality.

    For example,consider the following Objective-C method fromNSFileManager:

    1. - (BOOL)removeItemAtURL:(NSURL *)URL
    2. error:(NSError **)error;

    removeItemAtURL(URL: NSURL) throws

    Notice that theremoveItemAtURL(_:)method is imported by Swift with aVoidreturn type,noerrorparameter,and athrowsdeclaration.

    If the last non-block parameter of an Objective-C method is of typeNSError **,Swift replaces it with thethrowskeyword,to indicate that the method can throw an error. If the Objective-C method’s error parameter is also its first parameter,Swift attempts to simplify the method name further,by removing the “WithError” or “AndReturnError” suffix,if present,from the first part of the selector. If another method is declared with the resulting selector,the method name is not changed.

    If an error producing Objective-C method returns aBOOLvalue to indicate the success or failure of a method call,Swift changes the return type of the function toVoid. Similarly,if an error producing Objective-C method returns anilvalue to indicate the failure of a method call,Swift changes the return type of the function to a non-optional type.

    Otherwise,if no convention can be inferred,the method is left intact.

    Catching and Handling an Error

    ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,error handling is opt-in,meaning that errors produced by calling a method are ignored unless an error pointer is provided. In Swift,calling a method that throws requires explicit error handling.

    Here’s an example of how to handle an error when calling a method in Objective-C:

    1. NSFileManager *fileManager = [NSFileManager defaultManager];
    2. NSURL *fromURL = [NSURL fileURLWithPath:@"/path/to/old"];
    3. toURL = [@"/path/to/new"];
    4. NSError *error = nil;
    5. BOOL success = [fileManager moveItemAtURL:URL toURL:toURL error:&error];
    6. if (!success) {
    7. NSLog(@"Error: %@", error.domain);
    8. And here’s the equivalent code in Swift:

      fileManager = NSFileManager.defaultManager()
    9. fromURL = NSURL(fileURLWithPath: "/path/to/old")
    10. toURL = "/path/to/new")
    11. do {
    12. try fileManager.moveItemAtURL(fromURL,116)"> toURL: toURL)
    13. } catch error as NSError {
    14. print("Error: \(error.domain)")
    15. }

    Additionally,you can usecatchclauses to match on particular error codes as a convenient way to differentiate possible failure conditions:

    catch NSCocoaError.FileNoSuchFileError {
  • "Error: no such file exists")
  • FileReadUnsupportedSchemeError {
  • "Error: unsupported scheme (should be 'file://')")
  • }
  • Converting Errors to Optional Values

    ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,you passNULLfor the error parameter when you only care whether there was an error,not what specific error occurred. In Swift,you writetry?to change a throwing expression into one that returns an optional value,and then check whether the value isnil.

    ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,theNSFileManagerinstance methodURLForDirectory(_:inDomain:appropriateForURL:create:)returns a URL in the specified search path and domain,or produces an error if an appropriate URL does not exist and cannot be created. In Objective-C,the success or failure of the method can be determined by whether anNSURLobject is returned.

    1. tmpuRL = [fileManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
    2. tmpuRL != // ...
    3. You can do the same in Swift as follows:

      tmpuRL = try? URLForDirectory(.CachesDirectory,116)"> inDomain: .UserDomainMask,116)"> appropriateForURL: nil,116)"> create: true) {
    4. // ...
    5. }

    Throwing an Error

    If an error occurs in an Objective-C method,that error is used to populate the error pointer argument of that method:

    1. // an error occurred
    2. errorPtr) {
    3. *errorPtr = [NSError errorWithDomain:NSURLErrorDomain
    4. code:NSURLErrorCannotOpenFile
    5. userInfo: If an error occurs in a Swift method,the error is thrown,and automatically propagated to the caller:

      // an error occurred
    6. throw NSError(domain: NSURLErrorDomain,116)"> code: NSURLErrorCannotOpenFile,116)"> userInfo: nil)

    If Objective-C code calls a Swift method that throws an error,the error is automatically propagated to the error pointer argument of the bridged Objective-C method.

    ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,consider thereadFromFileWrapper(_:ofType:)method inNSDocument. In Objective-C,this method’s last parameter is of typeNSError **. When overriding this method in a Swift subclass ofNSDocument,the method replaces its error parameter and throws instead.

    SerializedDocument: NSDocument {
  • static ErrorDomain = "com.example.error.serialized-document"
  • representedobject: [String: AnyObject] = [:]
  • override func readFromFileWrapper(fileWrapper: NSFileWrapper,116)"> ofType typeName: String) throws {
  • guard data = fileWrapper.regularFileContents else {
  • nil)
  • }
  • case JSON as [ AnyObject] = try NSJSONSerialization.JSONObjectWithData(data,116)"> options: []) {
  • self.representedobject = JSON
  • else {
  • SerializedDocument.ErrorDomain,116)"> code: -1,145)"> nil)
  • If the method is unable to create an object with the regular file contents of the document,it throws anNSErrorobject. If the method is called from Swift code,the error is propagated to its calling scope. If the method is called from Objective-C code,the error instead populates the error pointer argument.

    ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,meaning that errors produced by calling a method are ignored unless you provide an error pointer. In Swift,calling a method that throws requires explicit error handling.

    Key-Value Observing

    Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects. You can use key-value observing with a Swift class,as long as the class inherits from theNSObjectclass. You can use these three steps to implement key-value observing in Swift.

    1. Add thedynamicmodifier to any property you want to observe. For more information ondynamic,seeRequiring Dynamic Dispatch.

      1. class MyObjectToObserve: NSObject {
      2. dynamic var myDate = NSDate()
      3. func updateDate() {
      4. myDate = }
      5. }
    2. Create a global context variable.

      1. private myContext = 0
    3. Add an observer for the key-path,override theobserveValueForKeyPath:ofObject:change:context:method,and remove the observer indeinit.

      MyObserver: var objectToObserve = MyObjectToObserve()
    4. override init() {
    5. super.init()
    6. objectToObserve.addobserver(self, forKeyPath: "myDate",116)"> options: .New,116)"> context: &myContext)
    7. func observeValueForKeyPath(keyPath: String?,116)"> ofObject object: AnyObject?,116)"> change: [String : AnyObject]?,116)"> context: UnsafeMutablePointer<Void>) {
    8. if context == &myContext {
    9. if let newValue = change?[NSkeyvalueChangeNewKey] {
    10. print("Date changed: \(newValue)")
    11. }
    12. } else {
    13. super.observeValueForKeyPath(keyPath,116)"> ofObject: object,116)"> change: change,116)"> context: context)
    14. deinit {
    15. removeObserver(myContext)
    16. }
  • Undo

    NSUndoManagerto allow users to reverse that operation’s effect. You can take advantage of Cocoa’s undo architecture in Swift just as you would in Objective-C.

    Objects in an app’s responder chain—that is,subclasses ofNSResponderon OS X andUIResponderon iOS—have a read-onlyundoManagerproperty that returns an optionalNSUndoManagervalue,which manages the undo stack for the app. Whenever an action is taken by the user,such as editing the text in a control or deleting an item at a selected row,an undo operation can be registered with the undo manager to allow the user to reverse the effect of that operation. An undo operation records the steps necessary to counteract its corresponding operation,such as setting the text of a control back to its original value or adding a deleted item back into a table.

    NSUndoManager supports two ways to register undo operations: a “simple undo",which performs a selector with a single object argument,and an “invocation-based undo",which uses anNSInvocationobject that takes any number and any type of arguments.

    Taskmodel,which is used by aToDoListControllerto display a list of tasks to complete:

    Task {
  • text: completed: Bool = false
  • init(text: String) {
  • text = text
  • ToDoListController: NSViewController,153)"> NSTableViewDataSource,153)"> NSTableViewDelegate {
  • @IBOutlet tableView: NSTableView!
  • tasks: [Task] = []
  • // ...
  • For properties in Swift,you can create an undo operation in thewillSetobserver usingselfas thetarget,the corresponding Objective-C setter as theselector,and the current value of the property as theobject:

    notesLabel: NSTextView!
  • notes: String? {
  • willSet {
  • undoManager?.registerUndoWithTarget( selector: "setNotes:",116)"> object: title)
  • setActionName(NSLocalizedString("todo.notes.update",116)"> comment: "Update Notes"))
  • didSet {
  • notesLabel.string = notes
  • For methods that take more than one argument,you can create an undo operation using anNSInvocation,which invokes the method with arguments that effectively revert the app to its previous state:

    remainingLabel: markTask(task: Task,116)"> asCompleted Bool) {
  • target = undoManager?.prepareWithInvocationTarget(self) as? ToDoListController {
  • target.markTask(task,116)"> asCompleted: !completed)
  • "todo.task.mark",22)"> "Mark As Completed"))
  • task.completed = completed
  • tableView.reloadData()
  • numberRemaining = tasks.filter{ $0.completed }.count
  • remainingLabel. String(format: NSLocalizedString("todo.task.remaining",22)"> "Tasks Remaining: %d"),116)"> numberRemaining)
  • TheprepareWithInvocationTarget(_:)method returns a proxy to the specifiedtarget. By casting toToDoListController,this return value can make the corresponding call tomarkTask(_:asCompleted:)directly.

    The Undo Architecture Programming Guide.

    Target-Action

    Target-action is a common Cocoa design pattern in which one object sends a message to another object when a specific event occurs. The target-action model is fundamentally similar in Swift and Objective-C. In Swift,you use theSelectortype to refer to Objective-C selectors. For an example of using target-action in Swift code,seeObjective-C Selectors.

    Singleton

    Singletons provide a globally accessible,shared instance of an object. You can create your own singletons as a way to provide a unified access point to a resource or service that’s shared across an app,such as an audio channel to play sound effects or a network manager to make HTTP requests.

    dispatch_oncefunction,which executes a block once and only once for the lifetime of an app:

    1. + (instancetype)sharedInstance {
    2. id _sharedInstance = static dispatch_once_t onceToken;
    3. dispatch_once(&onceToken, ^{
    4. _sharedInstance = [[self alloc] init];
    5. });
    6. _sharedInstance;
    7. Singleton {
    8. sharedInstance = Singleton()
    9. If you need to perform additional setup beyond initialization,you can assign the result of the invocation of a closure to the global constant:

      sharedInstance: Singleton = {
    10. instance = // setup code
    11. instance
    12. }()
    13. Type PropertiesinThe Swift Programming Language (Swift 2.1).

      Introspection

      isKindOfClass:method to check whether an object is of a certain class type,and theconformsToProtocol:method to check whether an object conforms to a specified protocol. In Swift,you accomplish this task by using theisoperator to check for a type,or theas?operator to downcast to that type.

      You can check whether an instance is of a certain subclass type by using theisoperator. Theisoperator returnstrueif the instance is of that subclass type,andfalseif it is not.

      if object is UIButton {
    14. // object is of type UIButton
    15. // object is not of type UIButton
    16. You can also try and downcast to the subclass type by using theas?operator. Theas?operator returns an optional value that can be bound to a constant using anif-letstatement.

      button = // object is successfully cast to type UIButton and bound to button
    17. // object could not be cast to type UIButton
    18. Type CastinginThe Swift Programming Language (Swift 2.1).

      Checking for and casting to a protocol follows exactly the same Syntax as checking for and casting to a class. Here is an example of using theas?operator to check for protocol conformance:

      dataSource = UITableViewDataSource {
    19. // object conforms to UITableViewDataSource and is bound to dataSource
    20. // object not conform to UITableViewDataSource
    21. Note that after this cast,monospace; word-wrap:break-word">dataSourceconstant is of typeUITableViewDataSource,so you can only call methods and access properties defined on theUITableViewDataSourceprotocol. You must cast it back to another type to perform other operations.

      ara" style="background-color:transparent; border:0px; font-size:1.4em; margin-top:0px; margin-bottom:15px; outline:0px; padding-top:0px; padding-bottom:0px; vertical-align:baseline; color:rgb(65,seeProtocolsinThe Swift Programming Language (Swift 2.1).

      Serializing

      Serialization allows you to encode and decode objects in your application to and from architecture-independent representations,such as JSON or property lists. These representations can then be written to a file or transmitted to another process locally or over a network.

      NSJSONSerialiationandNSPropertyListSerializationto initialize objects from a decoded JSON or property list serialization value—usually an object of typeNSDictionary<NSString *,id>. You can do the same in Swift,but because Swift enforces type safety,additional type casting is required in order to extract and assign values.

      Venuestructure,with anameproperty of typeString,acoordinateproperty of typeCLLocationCoordinate2D,monospace; word-wrap:break-word">categoryproperty of a nestedCategoryenumeration type :

      import Foundation
    22. CoreLocation
    23. struct Venue {
    24. enum Category: String {
    25. case Entertainment
    26. Food
    27. Nightlife
    28. Shopping
    29. name: String
    30. coordinates: CLLocationCoordinate2D
    31. category: Category
    32. An app that interacts withVenueinstances may communicate with a web server that vends JSON representations of venues,such as:

      1. {
      2. "name": "Caffe Macs",
      3. "coordinates": {
      4. "lat": 37.330576,monospace; word-wrap:break-word">"lng": -122.029739
      5. },monospace; word-wrap:break-word">"category": "Food"
      6. You can provide a failableVenueinitializer,which takes anattributesparameter of type[String : AnyObject]corresponding to the value returned fromNSJSONSerialiationorNSPropertyListSerialization:

        init?(attributes: [String : AnyObject]) {
      7. name = attributes["name"] String,116)"> coordinates = "coordinates"] as? [ Double],116)"> latitude = coordinates["lat"],116)"> longitude = "lng"],116)"> category = Category(rawValue: "category"] String ?? "Invalid")
      8. else {
      9. return nil
      10. name = name
      11. coordinates = CLLocationCoordinate2D(latitude: latitude,116)"> longitude: longitude)
      12. category = category
      13. Aguardstatement consisting of multiple optional binding expressions ensures that theattributesargument provides all of the required information in the expected format. If any one of the optional binding expressions fails to assign a value to a constant,monospace; word-wrap:break-word">guardstatement immediately stops evaluating its condition,and executes itselsebranch,which returns You can create aVenuefrom a JSON representation by creating a dictionary of attributes usingNSJSONSerializationand then passing that into the correspondingVenueinitializer:

        JSON = "{\"name\": \"Caffe Macs\",\"coordinates\": {\"lat\": 37.330576,\"lng\": -122.029739},\"category\": \"Food\"}"
      14. JSON.dataUsingEncoding(NSUTF8StringEncoding)!
      15. attributes = options: []) as! [ AnyObject]
      16. venue = Venue(attributes: attributes)!
      17. venue.name)
      18. // prints "Caffe Macs"

      Validating Serialized Representations

      In the prevIoUs example,monospace; word-wrap:break-word">Venueinitializer optionally returns an instance only if all of the required information is provided. If not,the initializer simply returns It is often useful to determine and communicate the specific reason why a given collection of values Failed to produce a valid instance. To do this,you can refactor the failable initializer into an initializer that throws:

      ValidationError: ErrorType {
    33. Missing(String)
    34. Invalid( AnyObject]) String ValidationError.Missing("name")
    35. Double] "coordinates")
    36. "lng"]
    37. else{
    38. Invalid( categoryName = "category")
    39. categoryName) Instead of capturing all of theattributesvalues at once in a singleguardstatement,this initializer checks each value individually and throws an error if any particular value is either missing or invalid.

      For instance,if the provided JSON didn’t have a value for the key"name",the initializer would throw the enumeration valueValidationError.Missingwith an associated value corresponding to the"name"field:

      1. "lat": 37.77492,monospace; word-wrap:break-word">"lng": -122.419
      2. "category": "Shopping"
      3. }
      "{\"coordinates\": {\"lat\": 37.7842,\"lng\": -122.4016},\"category\": \"Convention Center\"}"
    40. venue = attributes)
    41. Missing( field) {
    42. "Missing Field: \(field)// prints "Missing Field: name"

    Or,if the provided JSON specified all of the required fields,but had a value for the"category"key that didn’t correspond with therawValueof any of the definedCategorycases,monospace; word-wrap:break-word">ValidationError.Invalidwith an associated value corresponding to the"category"field:

    1. "name": "Moscone West",monospace; word-wrap:break-word">"lat": 37.7842,monospace; word-wrap:break-word">"lng": -122.4016
    2. "category": "Convention Center"
    3. "{\"name\": \"Moscone West\",\"coordinates\": {\"lat\": 37.7842,116)">Invalid("Invalid Field: \(// prints "Invalid Field: category"
    API Availability

    Some classes and methods are not available to all versions of all platforms that your app targets. To ensure that your app can accommodate any differences in functionality,you check the availability those APIs.

    respondsToSelector:andinstancesRespondToSelector:methods to check for the availability of a class or instance method. Without a check,the method call throws anNSinvalidargumentexception“unrecognized selector sent to instance” exception. For example,monospace; word-wrap:break-word">requestWhenInUseAuthorizationmethod is only available to instances ofCLLocationManagerstarting in iOS 8.0 and OS X 10.10:

    1. if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)]) {
    2. // Method is available for use.
    3. } else {
    4. // Method is not available.
    5. Here’s the prevIoUs example,in Swift:

      locationManager = CLLocationManager()
    6. locationManager.requestWhenInUseAuthorization()
    7. // error: only available on iOS 8.0 or newer

    If the app targets a version of iOS prior to 8.0 or OS X prior to 10.10,requestWhenInUseAuthorization()is unavailable,so the compiler reports an error.

    Swift code can use the availability of APIs as a conditionat run-time. Availability checks can be used in place of a condition in a control flow statement,such as anif,monospace; word-wrap:break-word">guard,orwhilestatement.

    Taking the prevIoUs example,you can check availability in anifstatement to callrequestWhenInUseAuthorization()only if the method is available at runtime:

    #available(iOS 8.0, OSX 10.10,*) {
  • Alternatively,you can check availability in acope unless the current target satisfies the specified requirements. This approach simplifies the logic of handling different platform capabilities.

    else { return }
  • requestWhenInUseAuthorization()
  • Each platform argument consists of one of platform names listed below,followed by corresponding version number. The last argument is an asterisk (*),which is used to handle potential future platforms.

    Platform Names:

    • iOS

    • iOSApplicationExtension

    • OSX

    • OSXApplicationExtension

    • watchOS

    • watchOSApplicationExtension

    • tvOS

    • tvOSApplicationExtension

    All of the Cocoa APIs provide availability information,so you can be confident the code you write works as expected on any of the platforms your app targets.

    You can denote the availability of your own APIs by annotating declarations with the@availableattribute. The@availableattribute uses the same Syntax as the#availableruntime check,with the platform version requirements provided as comma-delimited arguments.

    For example:

    @available( useShinyNewFeature() {
  • }
  • Processing Command-Line Arguments

    On OS X,you typically open an app by clicking its icon in the Dock or Launchpad,or by double-clicking its icon from the Finder. However,you can also open an app programmatically and pass command-line arguments from Terminal.

    You can get a list of any command-line arguments that are specified at launch by accessing theProcess.argumentstype property. This is equivalent to accessing theargumentsproperty onnsprocessInfo.processInfo().

    1. $ /path/to/app --argumentName value
    for argument in Process.arguments {
  • argument)
  • // prints "/path/to/app"
  • // prints "--argumentName"
  • // prints "value"
  • The first element inProcess.argumentsis always a path to the executable. Any command-line arguments that are specified at launch begin atProcess.arguments[1].

    相关文章

    软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
    现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
    前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
    【Android App】实战项目之仿抖音的短视频分享App(附源码和...
    前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
    因为我既对接过session、cookie,也对接过JWT,今年因为工作...