你好。 我叫 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