[代码审查挑战]解释第三个问题:注意执行时间#try!

你好。 我叫 haseken,负责 Yahoo! Car Navi 的 iOS 应用程序开发。

在前几天举行的 try!Swift Tokyo 2024 的 LINE Yahoo 企业展位上,我们举办了代码审查挑战赛。

代码审查挑战赛是一项公共代码审查,旨在将坏代码变成好代码。

在本次活动中,我们采取了与以往活动相同的举措,旨在让参与者对技术产生兴趣,并帮助员工从外部各方的评论中学习。

在本文中,我们将解释代码审查挑战的第三个问题。

问题代码

import UIKit
import StoreKit

// Check if there is a promotional app at the launch of the application by querying the server.
// If there is a promotional app, display PromotionStoreViewController modally.
final class AppTopViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await downloadPromotionAppInfo(url: URL(string: "
        }
    }

    private func downloadPromotionAppInfo(url: URL) async {
        guard let (data, response) = try? await URLSession.shared.data(from: url) else { return }

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            print("Invalid response")
            return
        }

        // json = ["appId": AppID of the promotional app (String),
        //         "appTitle": Name of the promotional app (String),
        //         "appIconImage": Image data of the promotional app (Data)]
        if let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            let viewController = PromotionStoreViewController()
            viewController.appId = jsonObject["appId"] as? String
            viewController.appTitleLabel.text = jsonObject["appTitle"] as? String
            if let imageData = jsonObject["appIconImage"] as? Data {
                viewController.appIconImageView.image = UIImage(data: imageData)
            }
            self.present(viewController, animated: true)
        }
    }

    // The rest of the code is omitted
}

final class PromotionStoreViewController: UIViewController {
    var appId: String?
    @IBOutlet var appTitleLabel: UILabel!
    @IBOutlet var appIconImageView: UIImageView!
    @IBOutlet var showAppStoreButton: UIButton!

    @IBAction private func didTapPresentStoreViewButton(sender: Any) {
        let storeViewController = SKStoreProductViewController()
        let parameters: [String: Any] = [SKStoreProductParameterITunesItemIdentifier: appId]
        storeViewController.loadProduct(withParameters: parameters) { status, error in
            if status {
                self.present(storeViewController, animated: true)
            } else {
                print(error!.localizedDescription)
            }
        }
    }
}

此代码适用于向用户介绍您要推广的应用的应用。

当您启动此应用程序时,它会使用 downloadPromotionAppInfo 查询服务器以查看是否有您要推广的应用程序,如果有,则显示 PromotionStoreViewController。

PromotionStoreViewController 显示 StoreKit 的 SKStoreProductViewController。

我试图在这段代码中引入一些问题。

存在强制解包、任务不保留、通信无法取消等问题,但在这篇文章中我想解释一下关于 UIKit 的以下三点。

  • UI 更改在主线程上进行
  • 如果在执行 viewDidLoad 之前访问 IBOutlet 会崩溃
  • 如果您在运行 viewDidAppear 之前呈现,则屏幕将不会显示

1. 在主线程上进行 UI 更改

更改 UI 时,建议在主线程上进行。

(UIViewController也可能是MainActor)

@MainActor
class UIViewController : UIResponder

(外部网站)

该代码可以在闭包内执行显示。

final class AppTopViewController: UIViewController {
     ...
    private func downloadPromotionAppInfo(url: URL) async {
        ... 
        if let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            let viewController = PromotionStoreViewController()
            viewController.appId = jsonObject["appId"] as? String
            viewController.appTitleLabel.text = jsonObject["appTitle"] as? String
            if let imageData = jsonObject["appIconImage"] as? Data {
                viewController.appIconImageView.image = UIImage(data: imageData)
            }
            self.present(viewController, animated: true)
        }
        ...
}

...

当处理这些时,使用主线程将导致以下结果。

// downloadPromotionAppInfo(url:)に@MainActorをつける
@MainActor
private func downloadPromotionAppInfo(url: URL) async {
    ...
}

// DispatchQueue.main.asyncでpresentを実行する
private func downloadPromotionAppInfo(url: URL) async {
    ...
    if let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
        let viewController = PromotionStoreViewController()
        ...
        DispatchQueue.main.async {
            self?.present(viewController, animated: true)
        }
    }             
}

2.执行viewDidLoad之前访问IBOutlet会崩溃

服务器通信完成后,会显示PromotionStoreViewController,但在此之前,会访问PromotionStoreViewController持有的IBOutlet。

final class AppTopViewController: UIViewController {
     ...
    private func downloadPromotionAppInfo(url: URL) async {
        ... 
        if let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
           ...
            viewController.appTitleLabel.text = jsonObject["appTitle"] as? String
            if let imageData = jsonObject["appIconImage"] as? Data {
                viewController.appIconImageView.image = UIImage(data: imageData)
            }
            self.present(viewController, animated: true)
        }
        ...
}

final class PromotionStoreViewController: UIViewController {
    ...
    @IBOutlet var appTitleLabel: UILabel!
    @IBOutlet var appIconImageView: UIImageView!
    ...
}

但是,直到 viewDidLoad 完成之前,IBOutlet 不会生成并变为 nil。

viewDidLoad是在pushViewController出现时调用的,但是这次IBOutlet是强制展开写入的,所以这次IBOutlet在viewDidLoad之前被访问,应用程序崩溃了。

如何处理此问题的示例如下。

// IBOutletをprivateにし、viewDidLoadでパラメータを設定する。
final class AppTopViewController: UIViewController {
     ...
    private func downloadPromotionAppInfo(url: URL) async {
        ... 
        if let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
           ...
            viewController.appTitle = jsonObject["appTitle"] as? String
            if let imageData = jsonObject["appIconImage"] as? Data {
                viewController.appIconImage= UIImage(data: imageData)
            }
            self.present(viewController, animated: true)
        }
        ...
}

final class PromotionStoreViewController: UIViewController {
    ...
    var appTitle: String?
    var appIconImage: UIImage?
    @IBOutlet private var appTitleLabel: UILabel!
    @IBOutlet private var appIconImageView: UIImageView!
    ...
    override func viewDidLoad() {
        super.viewDidLoad()

        appTitleLabel.text = appTitle
        appIconImageView.image = appIconImage 
    }
}

3.如果在执行viewDidAppear之前呈现,则屏幕不显示

在这段代码中,我们与服务器通信,检查通信结果,并显示PromotionStoreViewController。

但是如果present在viewDidAppear之前执行,那么你想要显示的UIViewController将不会显示。

这次,服务器通信一结束就执行present,因此是否执行viewDidAppear取决于例如网络速度的时机。

因此,应用程序每次运行时呈现的时间都会发生变化,这可能会引入错误,因此有必要更改通信开始或呈现的时间。

除此之外,我对评论的期望如下。

  • 有些部分是用 try 执行的。 由于不进行do-catch,所以最好实现错误处理处理。
  • 执行 JSONSerialization。 可以使用 Codable。 (优点比如可以使用Decoder.decode,变得更加抗可选)
  • 如果服务器端的JSON文件无效,则可能无法按预期返回JSON内容。那么,“appId”和“appTitle”可能没有值,这会影响显示的PromotionStoreViewController的显示内容和行为,所以最好实现错误处理处理。
  • “appId”是显示 SKStoreProductViewController 的必需参数,因此最好不要将其设为可选。
  • didTapPresentStoreViewButton 的 sender 参数没有使用,所以不需要写。
  • 在 didTapPresentStoreViewButton 中编写 IBAction 参数时,最好指定类型而不是 Any(在本例中为 UIButton)。

这些是我期待的一些复习点的答案。

大家觉得怎么样?
我们收到了现场很多人的好评!
(很高兴收到这样的评论,看不出问题!)

在我收到的众多评论中,有一些是我没想到的。
我想向您介绍其中一位。

不要在执行 async/await 的方法的名称中包含动词

这次,我们准备了一个方法downloadPromotionAppInfo来从服务器获取信息。

该方法的开头有动词“download”。
然而,在查看 Apple 的 API 时,有时带有 async 的方法名称不包含动词。例如,URLSession 的数据(for:delegate:)。

(外部网站)

如果我们按照Apple的方式编写API,不添加动词不是更好吗?这条评论出乎我的意料,所以我从中学到了很多东西。

综上所述

公司里有很多人审阅了这个示例代码。
如果没有这些人的评论,我认为示例代码的质量会更低。
我想再次表达我的谢意。
这是我第一次在公司展位上发布示例代码并让访客查看它。
起初,我担心我的代码可能太低级,如果没有得到任何评论我该怎么办,但是当我打开盖子时,我发现这么多人停下来问我评论后。在与同事协商后,他贴出了便利贴,指出了许多评论。
我觉得我找到了另一种参加会议的方式,我很高兴。

1713865053
#代码审查挑战解释第三个问题注意执行时间try
2024-04-23 02:00:00

Leave a Reply

Your email address will not be published. Required fields are marked *

近期新闻​

编辑精选​