GVKun编程网logo

Swift:UserDefaults 协议( Swift 视角下的泛字符串类型 API )(swift泛型协议的n种用法)

2

在这里,我们将给大家分享关于Swift:UserDefaults协议的知识,让您更了解Swift视角下的泛字符串类型API的本质,同时也会涉及到如何更有效地(三)宇宙大战SpaceBattle--场景

在这里,我们将给大家分享关于Swift:UserDefaults 协议的知识,让您更了解 Swift 视角下的泛字符串类型 API 的本质,同时也会涉及到如何更有效地(三) 宇宙大战 Space Battle -- 场景 SCENE 切换、UserDefaults 统计分数、Particle 粒子效果、Apifox:API 文档、API 调试、API Mock、API 自动化测试一体化协作平台、AudioFocusRequest.Builder() 需要 API 级别 26,但我想在 API 级别 22 上使用它,有什么方法可以在低于 API 26 的 API 级别上使用它吗?、Sheet 消失后将数据写入 UserDefaults 时崩溃 (SIGABRT)的内容。

本文目录一览:

Swift:UserDefaults 协议( Swift 视角下的泛字符串类型 API )(swift泛型协议的n种用法)

Swift:UserDefaults 协议( Swift 视角下的泛字符串类型 API )(swift泛型协议的n种用法)

无论是从语言本身还是项目代码,Swift3 的革新无疑是一场“惊天海啸” ,一些读者可能正奋战在代码迁移的前线。但即使有如此之多的改动, Swift 中依旧存在许多基于 Foundation 框架,泛字符串类型的 API 。这些 API 完全没有问题,只是...

我们对这种 API 有一种既爱又恨的感情:偏爱它的灵活性;又恨一时粗心导致问题接踵而来。这简直是在刀尖上编程。

Foundation 框架的开发者们之所以提供泛字符串类型的接口,是考虑到无法准确预见我们未来会如何使用这个框架。这些开发者们极尽自己的智慧、能力和知识,最终决定在某些 API 中使用字符串,这为我们开发人员带来了无尽的可能性,也可以说是一种黑魔法。

UserDefaults

今天的主题是我学习 iOS 开发初期最先熟悉的 API 之一。对于那些不熟悉它的人来说,它不过是对一系列信息的持久化存储,例如一张图片,一些应用的设置等。部分开发者偏向于认为它是"轻量级的 Core Data 。尽管人们绞尽脑汁想要把它作为替代品楔入,但结果表明它还远远不够强大。

Stringly typed API

UserDefaults.standard.set(true,forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")

这是 UserDefaults 在平常应用中的基础用法,它向我们提供了持久存储和取值的简单方法,在应用中随处可以覆盖或者删除数据。由于缺少一致性和上下文,我们一不小心就会犯错,但更有可能是拼写错误。在这篇文章当中,我们将会改变 UserDefaults 在通常意义上的特性,并根据我们的需要进行定制。

使用常量

let key = "isUserLoggedIn"
UserDefaults.standard.set(true,forKey: key)
UserDefaults.standard.bool(forKey: key)

如果你遵从这种奇妙的技巧,我保证你很快就能将代码写得更好。如果你需要多次重复使用一个字符串,那么将它转换成一个常量,并在你的余生一直遵守这种规则,然后记得下辈子谢谢我。

分组常量

struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true,forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)

一种可以帮我们维持一致性的模式就是将我们所有重要的默认常量分组写在同一个地方。这里我们创建了一个常量结构体来存储并指向我们的默认值。

还有一个建议是将你的属性名字设置成它对应的值,尤其是跟默认值打交道的时候。这样做可以简化你的代码并使属性在整体上有更好的一致性。拷贝属性名,将他们粘贴在字符串中,这样可以避免拼写错误。

let isUserLoggedIn = "isUserLoggedIn"

添加上下文

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...
UserDefaults.standard
   .set(true,forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)

创建一个常量结构体完全没有问题,但是在我们写代码的时候记得提供上下文。我们努力的目标是让自己的代码对任何人都具有较高的可读性,包括我们自己。

Constants().token // Huh?

token 是什么意思?当有人试图搞清楚这个 token 的意义是什么的时候,缺少命名空间上下文使得新人或者不熟悉代码的人很难搞清楚这意味着什么,甚至包括一年后的原作者。

Constants.Authentication().token // better

避免初始化

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
    private init() { }
}

我们绝对不打算,也不想让常量结构体被初始化,所以我们把初始化方法声明为私有方法。这只是一个预防性措施,但我仍然推荐这么做。至少这样做可以避免我们在只想要静态变量时却不小心声明了实例变量。说到静态变量...

静态变量

struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...
UserDefaults.standard
   .set(true,forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)

你可能已经注意到了,我们每次获取 key ,都需要初始化它所属的结构体。与其每次都这么做,我们不如把它声明为静态变量。

我们使用 static 而非 class 关键字,是因为结构体作为存储类型时只允许使用前者。依据 Swift 的编译规则,结构体不能使用 class 声明属性。但如果你在一个类中使用 static 声明属性,这跟使用 final class 声明属性是一样的。

final class name: String
static name: String
// final class == static

使用枚举类型避免拼写错误

enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...
UserDefaults.standard
    .set(true,forKey: Constants.Keys.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Keys.isUserLoggedIn.rawValue)

文章中我们提到了,为了一致性我们需要使属性能反映出他们的值。这里我们会将这种一致性更进一步,采用 enum case 来代替 static let 来将这个过程自动化。

你可能已经注意到了,我们已经创建了 Account 并让其遵守 String 协议,而 Stirng 遵守了 RawRepresentable 协议。这么做是因为,如果我们不给每个 case 提供一个 RawValue ,这个值将和声明的 case 保持一致。这么做会减少很多手动的输入或者复制粘贴字符串,减少错误的发生。

// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

到目前为止我们已经使用 UserDefaults 做了一些很酷的事情,但其实我们做的还不够。最大的问题是我们仍然在使用泛字符串类型 API ,即使我们已经对字符串做了一些修饰,但对于项目来说还不够好。

在我们的认知中,语言提供给我们什么,我们就只能干什么。然而 Swift 是一门如此棒的语言,我们已经在挑战过去写 Objective-C 时学习到和了解的知识。接下来,让我们回到厨房给这些 API 加些语法糖作料。

API 目标

UserDefaults.standard.set(true,forKey: .isUserLoggedIn) 
// #APIGoals

下面,我们会力争创建一些在与 UserDefaults 打交道时更好用的 API ,以此满足我们的需要。而比较好的做法莫过于使用协议扩展。

BoolUserDefaultable

protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}

首先我们来为布尔类型的 UserDefalts 创建一个协议,这个协议很简单,没有任何变量和需要实现的方法。然而,我们提供了一个叫做 BoolDefaultKey 的关联类型,这个类型遵守 RawRepresentable 协议,接下来你会明白为什么这么做。

扩展

extension BoolUserDefaultable 
    where BoolDefaultKey.RawValue == String { ... }

如果我们准备遵守协议的 Crusty 定律,首先声明一个协议扩展。并且使用一个 where 句法,限制扩展只适用于关联类型的 RawValue 是字符串的情况。

每一个协议,都有一个相当且相符合的协议扩展- Crusty 第三定律。

UserDefaultSetter 方法

// BoolUserDefaultable extension
static func set(_ value: Bool,forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value,forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}

是的,这是对标准 UserDefaultsAPI 的简单封装。我们这么做是因为这样代码的可读性会更高,因为你只需传入简单的枚举值而不需要传入冗长的字符串(校对者注:摒弃类似下面 Aint.Nobody.Got.Time.For.this.rawValue 这种路径式字符串)。

UserDefaults.set(false,forKey: Aint.Nobody.Got.Time.For.this.rawValue)

一致性

extension UserDefaults : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}

是的,我们准备扩展 UserDefaults ,让它遵守 BoolDefaultSettable 并提供一个名叫 BoolDefaultKey 的关联类型,这个关联类型遵守协议 RawRepresentable

// Setter
UserDefaults.set(true,forKey: .isUserLoggedIn)
// Getter
UserDefaults.bool(forKey: .isUserLoggedIn)

我们再一次挑战了只能使用已有 API 的规范,而定义了我们自己的 API 。这是因为,当我们扩展了 UserDefaults ,使用我们自己的 API 却丢失了上下文。如果这个 key 不是 .isUserLoggedIn ,我们还会理解它到底和什么关联么?

UserDefaults.set(true,forKey: .isAccepted) 
// Huh? isAccepted for what?

这个 key 的含义很模糊,它可能代表任何东西。即使看起来没什么,但提供上下文总是有好处的。

“有但是不需要”,比“不需要也没有”要好。

不用担心,添加上下文很简单。我们只需要给这个 key 添加一个命名空间。在这个例子中,我们创建了一个 Account 的命名空间,它包含了 isUserLoggedIn 这个 key

struct Account : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
    ...
}
...
Account.set(true,forKey: .isUserLoggedIn)

冲突

ley account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"

拥有两种分别遵守同一协议并提供了相同的 key 的类型绝对是有可能的,作为编程人员,如果我们不能在项目落地之前解决这个问题,那我们绝对要熬夜了。绝对不能冒着拿某个 key 改变另外一个 key 的值的风险。所以我们应该为我们自己的 key 创建命名空间。

命名空间

protocol KeyNamespaceable { }

我们肯定要为此创建一个协议了,谁叫咱们是 Swift 开发人员。协议通常是解决任何当前面临问题的首要尝试。如果协议是巧克力酱,我们就在所有的食物上面都抹上它,即使是牛排。知道我们有多爱协议了吗?

extension KeyNamespaceable { 
  func namespace<T>(_ key: T) -> String where T: RawRepresentable {
        return "\(Self.self).\(key.rawValue)"
  }
}

这是一个简单的方法,它将传入的字符串做了合并,并用"."来将这两个对象分开,一个是类的名字,一个是 keyRawValue 。我们也利用泛型来允许我们的方法接收一个遵守 RawRepresentable 协议的泛型参数 key

protocol BoolUserDefaultSettable : KeyNamespaceable

创建了命名空间协议之后,我们再来看之前的 BoolUserDefaultSettable 协议并让他遵守 KeyNamespaceable 协议,修改之前的扩展来让他发挥新功能的优势。

// BoolUserDefaultable extension
static func set(_ value: Bool,forKey key: BoolDefaultKey) {
    let key = namespace(key)
    UserDefaults.standard.set(value,forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)
    return UserDefaults.standard.bool(forKey: key)
}
...
ley account = namespace(Account.BoolDefaultKey.isUserLoggedIn)
let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)
// account != default
// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

上下文

由于创建了这个协议,我们可能会感觉从 UserDefaultsAPI 中解放了,也许会因此陶醉在协议的魅力之中。在这个过程中,我们通过将 key 移入有意义的命名空间来创建上下文。

Account.set(true,forKey: .isUserLoggedIn)

但由于这个 API 没有完整的意义我们还是一定程度上丢失了上下文。一眼看上去,代码中没有任何信息告诉我们这个布尔值会被持久存储。为了让一切圆满,我们准备扩展 UserDefaults 并把我们的默认类型放进去。

extension UserDefaults {
    struct Account : BoolUserDefaultSettable { ... }
}
...
UserDefaults.Account.set(true,forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg。

(三) 宇宙大战 Space Battle -- 场景 SCENE 切换、UserDefaults 统计分数、Particle 粒子效果

(三) 宇宙大战 Space Battle -- 场景 SCENE 切换、UserDefaults 统计分数、Particle 粒子效果

此《宇宙大战 Space Battle》SpirteKit 手机游戏教程共分为三系列:

(一) 宇宙大战 Space Battle -- 新建场景 Scene、精灵节点、Particle 粒子及背景音乐
(二) 宇宙大战 Space Battle -- 无限循环背景 Endless、SpriteKit 物理碰撞、CoreMotion 加速计
(三) 宇宙大战 Space Battle — 场景 SCENE 切换、UserDefaults 统计分数、Particle 粒子效果 (你正在此处进行学习)

一、如何进行各个场景之间的切换

 
场景 SCENE 切换

如上图所示,共分为三个场景:
1、MainScene.sks -- 用户打开 APP 时一开始看到的画面,等待用户点击 "Play" 按钮;
2、GameScene.sks -- 游戏进行中的场景画面,用于创建无限循环背景 Endless、监测 SpriteKit 物理碰撞、应用 CoreMotion 加速计,判断游戏的业务逻辑;
3、LoseScene.sks -- 游戏结束时的场景画面,记录当届分数,记录最高分并应用 UserDefaults 储存分数在手机沙盒当中,点击 "Tap to play" 按钮回到 GameScene 游戏场景画面;

 
目录文件在工程项目中的 Scenes 文件夹中

我们依据第一节所学到的知识,新建一个文件,在 Scenes 文件夹中,Mouse 右建 -> New File -> 选择 iOS -> SpriteKit Scene -> Next 命名一个新的场景为 MainScene.sks

 
新建一个文件
 
SpriteKit Scene
 
分别拖动三个 ColorSprite 到 MainScene.sks 场景中
 
三个精灵 Spirte 节点的 Zposition 为 1,位于背景的上方

另新建一个文件,也是在 Scenes 文件夹中,Mouse 右建 -> New File -> 选择 iOS -> Swift File -> Next 命名为 MainScene.swfit ,关联 MainScene.sks 的 Custom Class 为 MainScene.swift

 
关联 MainScene.sks 的 Custom Class 为 MainScene.swift

在 Game ViewController 设置开始场景为 MainScene.sks

 if let scene = MainScene(fileNamed: "MainScene") {
                scene.size = CGSize(width: 1536, height: 2048)
                scene.scaleMode = .aspectFill
                view.presentScene(scene)
            }

设置好启动场景后,我们再来 MainScene.swfit 编写代码:

在 didMove (to view: SKView) 里,

override func didMove(to view: SKView) {
        /// Play为场景命名的节点名称
        play = childNode(withName: "Play") as! SKSpriteNode
        learnTemp = childNode(withName: "learnTemp") as! SKSpriteNode // 背景音乐 let bgMusic = SKAudioNode(fileNamed: "spaceBattle.mp3") bgMusic.autoplayLooped = true addChild(bgMusic) } 

接下来,我们就要来判断用户的触摸位置,是不是按到 Play 按钮
在 override func touchesBegan (_ touches: Set<UITouch>, with event: UIEvent?) 函数里:

/// 判断用户是否有点击  
guard  let touch = touches.first else {
            return  
  }
  let touchLocation = touch.location(in: self) /// 获得点击的位置 if play.contains(touchLocation) { /// 表示触摸点击在play按钮当中 } 

把切换进入 GameScene.sks 的代码写在 play.contains (){} 函数里

let reveal = SKTransition.doorsOpenVertical(withDuration: TimeInterval(0.5))
           ///场景切换
            let mainScene = GameScene(fileNamed: "GameScene")
            mainScene?.size = self.size mainScene?.scaleMode = .aspectFill self.view?.presentScene(mainScene!, transition: reveal) 

这样子,我们就完成了场景之间的切换了!

同样,GameScene 切换到 LoseScene、LoseScene 切换为 GameScene 也是应用 self.view?.presentScene 的方法,具体代码如下:

在外星人 Alien 撞击到飞船,游戏结束,GameScene 切换到 LoseScene:

// MARK: 外星人Alien撞击到飞船,游戏结束
    func alienHitSpaceShip(nodeA:SKSpriteNode,nodeB:SKSpriteNode){
 // 切换游戏结束场景
                    let reveal = SKTransition.doorsOpenHorizontal(withDuration: TimeInterval(0.5)) let loseScene = LoseScene(fileNamed: "LoseScene") loseScene?.size = self.size loseScene?.scaleMode = .aspectFill self.view?.presentScene(loseScene!, transition: reveal) } 

二、应用 UserDefaults 储存游戏分数和最高分

我们在 GameScene.swift 里

 private var currentScore:SKLabelNode! // 当前分数节点
 private var cScore:Int = 0 /// Int 存当前分数 private var highScore:SKLabelNode! // 最高分数 private var hScore:Int = 0 /// Int 存最高分数 

在子弹击中外星人时记录分数
func bulletHitAlien(nodeA:SKSpriteNode,nodeB:SKSpriteNode){}

func bulletHitAlien(nodeA:SKSpriteNode,nodeB:SKSpriteNode){

  // 分数统计
        cScore += 1 currentScore.text = "SCORE:\(cScore)" // 保存当前分数 UserDefaults.standard.set(cScore, forKey: "CURRENTSCORE") if cScore > hScore { hScore = cScore highScore.text = "High:\(hScore)" // 保存最高分数 UserDefaults.standard.set(cScore, forKey: "HIGHSCORE") } } 

我们应用 UserDefaults.standard.set 方法,分别储存当前分数和最高分数对应的键值 forKey:CURRENTSCORE 和 HIGHSCORE,然后,在游戏结束的场景 LoseScene.swift 通过 UserDefaults.standard.integer (forKey: "CURRENTSCORE") 取出存在手机沙盒里的值;

currentScore.text = "SCORE:\(UserDefaults.standard.integer(forKey: "CURRENTSCORE"))"   // 取出当前分数
 highScore.text    = "HIGH SCORE:\(UserDefaults.standard.integer(forKey: "HIGHSCORE"))" // 取出沙盒中的最高分数 
 
取出沙盒中的分数,并分别把当前分数、最高分数显示在 LoseScene 场景当中

代码如下:

 private var currentScore:SKLabelNode! // 当局分数
 private var highScore:SKLabelNode! // 最高分数 override func didMove(to view: SKView) { // 找到 名称为Play的节点 play = childNode(withName: "Play") as! SKSpriteNode currentScore = childNode(withName: "currentScore") as! SKLabelNode highScore = childNode(withName: "highScore") as! SKLabelNode currentScore.text = "SCORE:\(UserDefaults.standard.integer(forKey: "CURRENTSCORE"))" // 取出当前分数 highScore.text = "HIGH SCORE:\(UserDefaults.standard.integer(forKey: "HIGHSCORE"))" // 取出沙盒中的最高分数 } 

我们补充一下有关 Swift 数据储存方式的相关知识,数据储存是存在 iOS 沙盒的当中,沙盒,顾名思义,即各个 app 之间是无法互相访问数据的,其目录结构为:

 
每个应用程序都有对应的私有目录,其根目录为 Home 目录。该目录下又三个文件夹:Documents、Library、tmp
 
UserDefaults 的存放位置

每个 iOS 应用都有自己的应用沙盒 (应用沙盒就是文件系统目录),与其他文件系统隔离。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。沙盒下的目录如下:

Documents: 保存应⽤运行时生成的需要持久化的数据,iTunes 同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录。

tmp: 保存应⽤运行时所需的临时数据,使⽤完毕后再将相应的文件从该目录删除。应用 没有运行时,系统也可能会清除该目录下的文件。iTunes 同步设备时不会备份该目录。

Library/Caches: 保存应用运行时⽣成的需要持久化的数据,iTunes 同步设备时不会备份 该目录。⼀一般存储体积大、不需要备份的非重要数据,比如网络数据缓存存储到 Caches 下。

Library/Preference: 保存应用的所有偏好设置,如 iOS 的 Settings (设置) 应⽤会在该目录中查找应⽤的设置信息。iTunes 同步设备时会备份该目录。

UserDefaults 可以存储的数据类型:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary,如果把有 null 的 value 放入 userDefaults,程序会崩。

//存储基础类型,以Int为例。
UserDefaults.standard.set(15, forKey:"theKey")

//读取基础类型,以Int为例。 let num = UserDefaults.standard.integer(forKey: "theKey") 

注意:不要用 UserDefaults 储存用户的密码。

三、SpirteKit Particle 粒子效果

我们选中 Explosion.sks 来查看 Particle 粒子效果,右侧为粒子效果的属性面板,我们可以修改属性中的相关值,来观察爆炸的变化。

 
Particle 粒子效果

在代码中引入,外星人 Alien 撞击到飞船引入粒子特效。

// 击中粒子效果 Particle
func alienHitSpaceShip(nodeA:SKSpriteNode,nodeB:SKSpriteNode){
if (nodeA.physicsBody?.categoryBitMask == PhysicsCategory.Alien  || nodeB.physicsBody?.categoryBitMask == PhysicsCategory.Alien) && (nodeA.physicsBody?.categoryBitMask == PhysicsCategory.SpaceShip || nodeB.physicsBody?.categoryBitMask == PhysicsCategory.SpaceShip) {

            let explosion = SKEmitterNode(fileNamed: "Explosion")! explosion.position = nodeA.position self.addChild(explosion) } } 

子弹击中外星飞船的粒子特效

 // MARK: 子弹vs外星人
    func bulletHitAlien(nodeA:SKSpriteNode,nodeB:SKSpriteNode){
        
        // 击中粒子效果 Particle
        let explosion = SKEmitterNode(fileNamed: "ExplosionBlue")! explosion.position = nodeA.position self.addChild(explosion) explosion.run(SKAction.sequence([ SKAction.wait(forDuration: 0.3), SKAction.run { explosion.removeFromParent() }])) } 

子弹引入粒子特效,在 func spawnBulletAndFire (){} 里

/*
         * 粒子效果
         * 1.新建一个SKNode => trailNode
         * 2.新建粒子效果SKEmitterNode,设置tragetNode = trailNode
         * 3.子弹加上emitterNode
         */
        let trailNode = SKNode()
        trailNode.zPosition = 1
        trailNode.name = "trail"
        addChild(trailNode)
        
        let emitterNode = SKEmitterNode(fileNamed: "ShootTrailBlue")! // particles文件夹存放粒子效果 emitterNode.targetNode = trailNode // 设置粒子效果的目标为trailNode => 跟随新建的trailNode bulletNode.addChild(emitterNode) // 在子弹节点Node加上粒子效果; 

如果去除 emitterNode.targetNode = trailNode,则子弹没有拖影特效

其中特别要注意的是要判断哪个是子弹精灵节点,并移除所有子弹精灵的子节点的特效。

nodeA.removeAllChildren () // 移除所有子效果 emitter

 // 判断哪个是子弹节点bulletNode,碰撞didBegin没有比较大小时,则会相互切换,也就是A和B互相切换;
        if nodeA.physicsBody?.categoryBitMask == PhysicsCategory.BulletBlue {
            nodeA.removeAllChildren() // 移除所有子效果 emitter
            nodeA.isHidden = true     // 子弹隐藏 nodeA.physicsBody?.categoryBitMask = 0 // 设置子弹不会再发生碰撞 nodeB.removeFromParent() // 移除外星人 }else if nodeB.physicsBody?.categoryBitMask == PhysicsCategory.BulletBlue { nodeA.removeFromParent() // 移除外星人 nodeB.removeAllChildren() nodeB.isHidden = true nodeB.physicsBody?.categoryBitMask = 0 } 

至此,我们就完成了 Space Battle 宇宙大战 SpirteKit 游戏的所有章节了。

很感谢大家的阅读,如果有疑问可以在文章下方的评论栏提问,也请大家多多指出文中的不足之处,一起努力,一起从开发游戏当中获得满满的乐趣,收获满满的自豪感。

游戏源码传送门:https://github.com/apiapia/SpaceBattleSpriteKitGame
更多游戏教程:http://www.iFIERO.com

Apifox:API 文档、API 调试、API Mock、API 自动化测试一体化协作平台

Apifox:API 文档、API 调试、API Mock、API 自动化测试一体化协作平台

我是 ABin-阿斌:写一生代码,创一世佳话,筑一览芳华。 如果小伙伴们觉得文章有点 feel ,那就点个赞再走哦。

在这里插入图片描述

声明:原位地址:https://blog.csdn.net/web15286201346/article/details/126098695

文章目录

  • 一、apifox简介及下载:
  • 二、apifox页面布局简介:
    • 1、apifox几个简单概念:
    • 2、以项目单位分组
    • 3、点击项目后进入项目,在该项目下管理接口。
      • 1、新建接口
      • 2、修改接口:
      • 3、运行接口:
      • 4、断言:
      • 5、批量运行:
  • 三、 总结

一、apifox简介及下载:

1、apifox:是 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台

2、定位 :Postman + Swagger + Mock + JMeter

3、下载与安装:

官网下载地址:https://www.apifox.cn/

按照需要下载对应版本,下载完毕后解压安装即可。

二、apifox页面布局简介:

1、apifox几个简单概念:

(1)团队:该工具支持团队协同办公,可以根据需要 创建不同的团队,在工具页面左侧,显示自己的团队,也可新建团队

新建团队,需要一个团队 名称:

创建成功团队后,可以邀请成员、设置权限等,或删除团队

有了团队,就可以开始我们接口的管理及测试工作了。

2、以项目单位分组

  • apifox是以团队下项目来管理接口的,将所需接口维护在项目中,在不同的项目中对 接口进行维护及操作。

3、点击项目后进入项目,在该项目下管理接口。

1、新建接口

  • 维护接口信息,包括接口URL,接口基础信息,请求参数等,需要注意的是,此处只维护接口信息,类似于接口文档,不运行接口

接口URL,http协议及域名部分,建议设置在环境中,页面右上角选择环境处,可维护环境信息,因为我们在测试工作中,往往会有多个环境,将协议及域名维护在环境中,测试不同环境的同一个接口时,只需要切换环境即可,不用不同环境维护不同的接口。

对于需要cookie的接口,在维护接口时,请求参数中,别忘了维护cookie信息。

2、修改接口:

在接口管理-修改文档下,可修改已维护的接口信息

3、运行接口:

  • 接口运行,往往是依据测试用例,在接口测试中,可以简单的认为不同的传值即为不同的测试用例,apifox中,运行接口的入口在项目中,接口管理-运行下,在此处修改参数值,点击发送后,可已看到返回信息
  • 此外,可将运行数据保存为用例,保存成功后,此次运行的数据会保存,下次打开该用例,其中参数值可复用(注:运行接口时,需要选择环境)

若设置了断言,可根据断言判断此条用例是否通过:

修改了参数值信息,需要点击保存才能更新成功,若不保参数值依然为修改前值。

测试用例显示在接口的下一级,可通过复制用例的方式,维护多个用例。

4、断言:

对测试用例,可以设置其断言,即期望结果,apifox在后置操作中进行断言

apifox断言核心为提取表达式,该提取表达式很简单,即将目标返回字段提取出来,$及为根节点,一级一级定位到目标字段即可

举个例子:若返回信息如下图所示,我想通过sort_finish字段值断言,则提取该字段的表达式为:$.data.sort_data.Box_no

5、批量运行:

  • apifox的批量运行,在自动化测试页面,可在该页面添加一个分组,在分组下添加测试用例,创建完测试用例后进入所创用例,即可添加步骤,此时可导入接口用例


导入用例后,可根据需要设置循环次数及线程数等信息,点击运行,即可批量执行,执行完成后,显示此次执行结果:

三、 总结

  • 以上:为 apifox 基本使用功能,变量提取、套件使用等,待续~

AudioFocusRequest.Builder() 需要 API 级别 26,但我想在 API 级别 22 上使用它,有什么方法可以在低于 API 26 的 API 级别上使用它吗?

AudioFocusRequest.Builder() 需要 API 级别 26,但我想在 API 级别 22 上使用它,有什么方法可以在低于 API 26 的 API 级别上使用它吗?

如何解决AudioFocusRequest.Builder() 需要 API 级别 26,但我想在 API 级别 22 上使用它,有什么方法可以在低于 API 26 的 API 级别上使用它吗?

我正在开发一个媒体播放器应用程序,我希望它支持的最小 API 级别是 API 22。

我想使用 Audio Focus 进行媒体播放,但是 AudioFocusRequest.Builder() 在低于 API 26 的 API 级别上不起作用,并且曾经在低于 API 26 的 API 级别上工作的方法 requestAudioFocus() 是现在已弃用,那么我如何在应用中使用 Audio Focus 以使其适用于低于 API 26 的 API 级别以及更高的 API 级别?

  1. audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUdioFOCUS_GAIN)
  2. .setonAudioFocuschangelistener(audioFocuschangelistener)
  3. .setFocusGain(AudioManager.AUdioFOCUS_GAIN)
  4. .build();

这段代码不适用于低于 26 的 API 级别并给出此错误消息,

“调用需要 API 级别 26(当前最小值为 22):新的 android.media.AudioFocusRequest.Builder”

解决方法

Check the API level at runtime 选择使用 requestAudioFocus() 还是 AudioFocusRequest.Builder

  1. if (Build.VERSION.SDK_INT >= 26) {
  2. // Use AudioFocusRequest.Builder
  3. } else {
  4. // Use requestAudioFocus
  5. }

这应该会使您收到的错误和 IDE 中的弃用消息都清除。

Sheet 消失后将数据写入 UserDefaults 时崩溃 (SIGABRT)

Sheet 消失后将数据写入 UserDefaults 时崩溃 (SIGABRT)

如何解决Sheet 消失后将数据写入 UserDefaults 时崩溃 (SIGABRT)

我收到了三个无法重现的类似崩溃报告(均在 iOS 14.4 上)。 stracktrace 说明如下(我只粘贴了我的应用程序启动的部分):

  1. Exception Type: EXC_CRASH (SIGABRT)
  2. Exception Codes: 0x0000000000000000,0x0000000000000000
  3. Exception Note: EXC_CORPSE_NOTIFY
  4. Triggered by Thread: 0
  5. Thread 0 name:
  6. Thread 0 Crashed:
  7. 0 libsystem_kernel.dylib 0x00000001c077d414 __pthread_kill + 8
  8. 1 libsystem_pthread.dylib 0x00000001de2d8b50 pthread_kill + 272 (pthread.c:1392)
  9. 2 libsystem_c.dylib 0x000000019bc5bb74 abort + 104 (abort.c:110)
  10. 3 libswiftCore.dylib 0x0000000196795f20 swift::fatalError(unsigned int,char const*,...) + 60 (Errors.cpp:393)
  11. 4 libswiftCore.dylib 0x0000000196796078 swift::swift_abortRetainUNowned(void const*) + 36 (Errors.cpp:460)
  12. 5 libswiftCore.dylib 0x00000001967e5844 swift_unkNownObjectUNownedLoadStrong + 76 (SwiftObject.mm:895)
  13. 6 SwiftUI 0x00000001992b0cdc ViewGraph.graphDelegate.getter + 16 (ViewGraph.swift:234)
  14. 7 SwiftUI 0x00000001997e4d58 closure #1 in GraphHost.init(data:) + 80
  15. 8 SwiftUI 0x00000001997e6550 partial apply for closure #1 in GraphHost.init(data:) + 40 (<compiler-generated>:0)
  16. 9 AttributeGraph 0x00000001bbcc9b88 AG::Graph::Context::call_update() + 76 (ag-closure.h:108)
  17. 10 AttributeGraph 0x00000001bbcca1a0 AG::Graph::call_update() + 56 (ag-graph.cc:176)
  18. 11 AttributeGraph 0x00000001bbccfd70 AG::Subgraph::update(unsigned int) + 92 (ag-graph.h:709)
  19. 12 SwiftUI 0x00000001997e1cdc GraphHost.runTransaction() + 172 (GraphHost.swift:491)
  20. 13 SwiftUI 0x00000001997e4e1c GraphHost.runTransaction(_:) + 92 (GraphHost.swift:471)
  21. 14 SwiftUI 0x00000001997e37a8 GraphHost.flushTransactions() + 176 (GraphHost.swift:459)
  22. 15 SwiftUI 0x00000001997e2c78 specialized GraphHost.asyncTransaction<A>(_:mutation:style:) + 252 (<compiler-generated>:0)
  23. 16 SwiftUI 0x00000001993bd2fc AttributeInvalidatingSubscriber.invalidateAttribute() + 236 (AttributeInvalidatingSubscriber.swift:89)
  24. 17 SwiftUI 0x00000001993bd1f8 AttributeInvalidatingSubscriber.receive(_:) + 100 (AttributeInvalidatingSubscriber.swift:53)
  25. 18 SwiftUI 0x00000001993bd914 protocol witness for Subscriber.receive(_:) in conformance AttributeInvalidatingSubscriber<A> + 24 (<compiler-generated>:0)
  26. 19 SwiftUI 0x000000019956ba34 SubscriptionLifetime.Connection.receive(_:) + 100 (SubscriptionLifetime.swift:195)
  27. 20 Combine 0x00000001a6e67900 ObservableObjectPublisher.Inner.send() + 136 (ObservableObject.swift:115)
  28. 21 Combine 0x00000001a6e670a8 ObservableObjectPublisher.send() + 632 (ObservableObject.swift:153)
  29. 22 Combine 0x00000001a6e4ffdc PublishedSubject.send(_:) + 136 (PublishedSubject.swift:82)
  30. 23 Combine 0x00000001a6e76994 specialized static Published.subscript.setter + 388 (Published.swift:0)
  31. 24 Combine 0x00000001a6e75f74 static Published.subscript.setter + 40 (<compiler-generated>:0)
  32. 25 MyApp 0x00000001005d1228 counter.set + 32 (Preferences.swift:0)
  33. 26 MyApp 0x00000001005d1228 Preferences.counter.modify + 120 (Preferences.swift:0)
  34. 27 MyApp 0x00000001005ca440 MyView.changeCounter(decrease:) + 344 (MyView.swift:367)
  35. 28 MyApp 0x00000001005cf110 0x100584000 + 307472
  36. 29 MyApp 0x00000001005e65d8 thunk for @escaping @callee_guaranteed () -> () + 20 (<compiler-generated>:0)
  37. 30 MyApp 0x00000001005a8828 closure #2 in MySheet.body.getter + 140 (MySheet.swift:0)

发生的事情是,我有一个带有按钮的工作表,单击它时工作表消失了,并且在 ondisappear 中,主视图 changeCounter 中的 MyView 方法被调用更改 counter。调用/打开工作表时,方法 changeCounterMyView 传递到工作表。

这是 MyView 中的 .sheet 方法:

  1. .sheet(item: $activeSheet) { item in
  2. switch item {
  3. case .MY_SHEET:
  4. MySheet(changeCounter: {changeCounter(decrease: true)},changeTimer,item: $activeSheet)
  5. }
  6. }

这是(该表的重要部分):

  1. struct MySheet: View {
  2. var changeCounter: () -> Void
  3. var changeTimer: () -> Void
  4. @Binding var item: ActiveSheet?
  5. @State var dismissAction: (() -> Void)?
  6. var body: some View {
  7. GeometryReader { metrics in
  8. vstack {
  9. Button(action: {
  10. self.dismissAction = changeCounter
  11. self.item = nil
  12. },label: {
  13. Text("change_counter")
  14. })
  15. Button(action: {
  16. self.dismissAction = changeTimer
  17. self.item = nil
  18. },label: {
  19. Text("change_timer")
  20. })
  21. }.frame(width: metrics.size.width,height: metrics.size.height * 0.85)
  22. }.ondisappear(perform: {
  23. if self.dismissAction != nil {
  24. self.dismissAction!()
  25. }
  26. })
  27. }
  28. }

这是带有 changeCounter 对象的 preferences

  1. struct MyView: View {
  2. @EnvironmentObject var preferences: Preferences
  3. var body: some View {...}
  4. func changeCounter(decrease: Bool) {
  5. if decrease {
  6. preferences.counter -= COUNTER_INTERVAL
  7. }
  8. }
  9. }

Preferences 是带有 ObservableObject 变量的 counter

  1. class Preferences: ObservableObject {
  2. let userDefaults: UserDefaults
  3. init(_ userDefaults: UserDefaults) {
  4. self.userDefaults = userDefaults
  5. self.counter = 0
  6. }
  7. @Published var counter: Int {
  8. didSet {
  9. self.userDefaults.set(counter,forKey: "counter")
  10. }
  11. }
  12. }

它更改了 userDefaults 中的一个值为 UserDefaults.standard

任何人都知道崩溃是如何发生的以及在什么情况下发生的?因为它现在只在用户设备上发生了 3 次,我无法重现。

解决方法

分析一下

  1. Button(action: {
  2. self.dismissAction = changeCounter 1)
  3. self.item = nil 2)
  4. },label: {

第 1 行更改内部工作表状态,启动工作表视图的更新 第 2 行)更改外部状态以启动工作表的关闭(以及可能更新父视图)。

这甚至听起来像是两个相互冲突的过程(即使没有相关的流程,但第二个查看您的代码取决于第一个的结果)。所以,这是非常危险的逻辑,应该避免。

总的来说,正如我在评论中所写的那样,在一个闭包中改变两个状态总是有风险的,所以我会重写逻辑以获得类似(草图)的内容:

  1. Button(action: {
  2. self.result = changeCounter // one external binding !!
  3. },label: {

,即。启动一些外部活动的一种状态变化...

您的代码的可能解决方法(如果由于任何原因您无法更改逻辑)是及时分离这些状态的更改,例如

  1. Button(action: {
  2. self.dismissAction = changeCounter // updates sheet
  3. DispatchQueue.main.async { // or after some min delay
  4. self.item = nil // closes sheet after (!) update
  5. }
  6. },label: {

今天关于Swift:UserDefaults 协议 Swift 视角下的泛字符串类型 API 的讲解已经结束,谢谢您的阅读,如果想了解更多关于(三) 宇宙大战 Space Battle -- 场景 SCENE 切换、UserDefaults 统计分数、Particle 粒子效果、Apifox:API 文档、API 调试、API Mock、API 自动化测试一体化协作平台、AudioFocusRequest.Builder() 需要 API 级别 26,但我想在 API 级别 22 上使用它,有什么方法可以在低于 API 26 的 API 级别上使用它吗?、Sheet 消失后将数据写入 UserDefaults 时崩溃 (SIGABRT)的相关知识,请在本站搜索。

本文标签: