├── V2EX
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ ├── Icon-76.png
│ │ ├── Icon-1024.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-76@2x.png
│ │ ├── Icon-Small.png
│ │ ├── Icon-83.5@2x.png
│ │ ├── Icon-Small-20.png
│ │ ├── Icon-Small@2x.png
│ │ ├── Icon-Small@3x.png
│ │ ├── Icon-Small-20@2x.png
│ │ ├── Icon-Small-20@3x.png
│ │ ├── Icon-Small-40@2x.png
│ │ └── Contents.json
├── Classes
│ ├── Models
│ │ ├── Section.swift
│ │ ├── Comment.swift
│ │ ├── Node.swift
│ │ ├── Reply.swift
│ │ ├── Session.swift
│ │ ├── User.swift
│ │ └── Topic.swift
│ ├── Views
│ │ ├── UIImageView.swift
│ │ ├── UIView.swift
│ │ ├── Topic
│ │ │ ├── TopicBodyCell.swift
│ │ │ ├── TopicCommentCell.swift
│ │ │ ├── TopicNameCell.swift
│ │ │ ├── TopicReplyCell.swift
│ │ │ └── TopicWebCell.swift
│ │ ├── ActivityIndicatorView.swift
│ │ ├── NoContentView.swift
│ │ ├── NetworkErrorView.swift
│ │ ├── Topics
│ │ │ ├── TopicsNodesView.swift
│ │ │ ├── TopicsNodeButton.swift
│ │ │ └── TopicsCell.swift
│ │ ├── SignIn
│ │ │ ├── SignInPasswordCell.swift
│ │ │ ├── SignInUsernameCell.swift
│ │ │ └── SignInCaptchaCell.swift
│ │ └── User
│ │ │ └── UserHeaderView.swift
│ └── Controllers
│ │ ├── ViewController.swift
│ │ ├── ComposeController.swift
│ │ ├── WebViewController.swift
│ │ ├── MoreController.swift
│ │ ├── NodesController.swift
│ │ ├── SignInController.swift
│ │ ├── UserController.swift
│ │ ├── TopicsController.swift
│ │ └── TopicController.swift
├── AppDelegate.swift
├── Info.plist
└── zh-Hans.lproj
│ └── LaunchScreen.storyboard
├── V2EX.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── swordray.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── xcshareddata
│ └── xcschemes
│ │ └── V2EX.xcscheme
└── project.pbxproj
├── README.md
├── V2EX.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile
├── LICENSE
├── .gitignore
├── .swiftlint.yml
└── Podfile.lock
/V2EX/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20@2x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-20@3x.png
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swordray/v2ex-ios/HEAD/V2EX/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
--------------------------------------------------------------------------------
/V2EX.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # v2ex-ios
2 |
3 | A V2EX iOS App.
4 |
5 | ## Sponsors
6 |
7 | * [BaiLu ShuYuan](https://bailushuyuan.org)
8 |
9 | ## License
10 |
11 | Copyright © 2019 Jianqiu Xiao under The [MIT License](http://opensource.org/licenses/MIT).
12 |
--------------------------------------------------------------------------------
/V2EX.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/V2EX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Section.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Section.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/19/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Section: JSONCodable {
12 |
13 | var name: String?
14 | var nodes: [Node]
15 | }
16 |
--------------------------------------------------------------------------------
/V2EX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comment.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/17/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Comment: JSONCodable {
12 |
13 | var bodyHTML: String?
14 | var createdAt: String?
15 | var index: Int?
16 | }
17 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Node: JSONCodable {
12 |
13 | var name: String?
14 | var code: String?
15 |
16 | static var all = Node(name: "全部", code: "all")
17 | }
18 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Reply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reply.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/10/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Reply: JSONCodable {
12 |
13 | var bodyHTML: String?
14 | var createdAt: String?
15 | var user: User?
16 | var index: Int?
17 | }
18 |
--------------------------------------------------------------------------------
/V2EX.xcodeproj/xcuserdata/swordray.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | V2EX.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 17
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Session.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Session.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/17/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Session: JSONCodable {
12 |
13 | var usernameKey: String
14 | var passwordKey: String
15 | var captchaKey: String
16 | var once: String
17 | var username: String?
18 | var password: String?
19 | var captcha: String?
20 | }
21 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '13.0'
2 |
3 | target 'V2EX' do
4 | use_frameworks!
5 |
6 | pod 'Alamofire', '~> 5.0.0-rc.2'
7 | pod 'AlamofireImage', '~> 4.0.0-beta.5'
8 | pod 'CrossroadRegex'
9 | pod 'Firebase/Core'
10 | pod 'JGProgressHUD'
11 | pod 'JXWebViewController'
12 | pod 'Kanna'
13 | pod 'KeychainAccess'
14 | pod 'Ladybug'
15 | pod 'SnapKit'
16 | pod 'SwiftDate'
17 | pod 'SwiftLint'
18 | pod 'TUSafariActivity'
19 | pod 'UIButtonSetBackgroundColorForState'
20 | end
21 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/UIImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 6/4/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import AlamofireImage
10 |
11 | extension UIImageView {
12 |
13 | internal func setImage(withURL url: URL?) {
14 | if let url = url {
15 | af_setImage(withURL: url, placeholderImage: UIImage())
16 | } else {
17 | image = nil
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/UIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/10/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIResponder {
12 |
13 | internal func next(of type: T.Type) -> T? {
14 | return next as? T ?? next?.next(of: type)
15 | }
16 | }
17 |
18 | extension UIView {
19 |
20 | internal var viewController: ViewController? {
21 | return next(of: ViewController.self)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct User: JSONCodable {
12 |
13 | var id: Int?
14 | var name: String?
15 | var avatar: URL?
16 | var isBlocked: Bool?
17 | var token: String?
18 | var once: String?
19 | var createdAt: Date?
20 | var topics: [Topic]?
21 |
22 | static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
23 | "createdAt": "createdAt" <- format("yyyy-MM-dd HH:mm:ss ZZZZZ"),
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/V2EX/Classes/Models/Topic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Topic.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Ladybug
10 |
11 | struct Topic: JSONCodable {
12 |
13 | var id: Int?
14 | var name: String?
15 | var bodyHTML: String?
16 | var clicksCount: Int?
17 | var favoritesCount: Int?
18 | var repliesCount: Int?
19 | var repliesNextPage: Int?
20 | var repliedAt: String?
21 | var createdAt: String?
22 | var isSticky: Bool?
23 | var isFavorite: Bool?
24 | var favoriteToken: String?
25 | var once: Int?
26 | var user: User?
27 | var node: Node?
28 | var comments: [Comment]?
29 | }
30 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topic/TopicBodyCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BodyCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/10/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicBodyCell: TopicWebCell {
12 |
13 | public var topic: Topic? { didSet { didSetTopic() } }
14 |
15 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
16 | super.init(style: style, reuseIdentifier: reuseIdentifier)
17 |
18 | contentView.addSubview(webView)
19 | webView.snp.makeConstraints { $0.edges.equalTo(contentView.layoutMarginsGuide).priority(999) }
20 | }
21 |
22 | private func didSetTopic() {
23 | bodyHTML = topic?.bodyHTML
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/ActivityIndicatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicatorView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/11/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ActivityIndicatorView: UIActivityIndicatorView {
12 |
13 | init() {
14 | super.init(style: .medium)
15 | }
16 |
17 | @available(*, unavailable)
18 | required init(coder: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | override func didMoveToSuperview() {
23 | super.didMoveToSuperview()
24 |
25 | if let superview = superview {
26 | snp.makeConstraints { $0.center.equalTo(superview.safeAreaLayoutGuide) }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/NoContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotFoundView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/11/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NoContentView: UIStackView {
12 |
13 | public var textLabel: UILabel!
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | alignment = .center
19 |
20 | isHidden = true
21 |
22 | textLabel = UILabel()
23 | textLabel.font = .preferredFont(forTextStyle: .title1)
24 | textLabel.textColor = .secondaryLabel
25 | addArrangedSubview(textLabel)
26 | }
27 |
28 | @available(*, unavailable)
29 | required init(coder: NSCoder) {
30 | fatalError("init(coder:) has not been implemented")
31 | }
32 |
33 | override func didMoveToSuperview() {
34 | super.didMoveToSuperview()
35 |
36 | if let superview = superview {
37 | snp.makeConstraints { $0.center.equalTo(superview.safeAreaLayoutGuide) }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Jianqiu Xiao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/NetworkErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkErrorView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/11/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkErrorView: UIView {
12 |
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 |
16 | isHidden = true
17 |
18 | let label = UILabel()
19 | label.backgroundColor = .systemGray
20 | label.clipsToBounds = true
21 | label.font = .boldSystemFont(ofSize: 17)
22 | label.layer.cornerRadius = 10
23 | label.text = "!"
24 | label.textAlignment = .center
25 | label.textColor = .white
26 | addSubview(label)
27 | label.snp.makeConstraints { make in
28 | make.center.equalToSuperview()
29 | make.size.equalTo(20)
30 | }
31 | }
32 |
33 | @available(*, unavailable)
34 | required init?(coder aDecoder: NSCoder) {
35 | fatalError("init(coder:) has not been implemented")
36 | }
37 |
38 | override func didMoveToSuperview() {
39 | super.didMoveToSuperview()
40 |
41 | if let superview = superview {
42 | snp.makeConstraints { $0.edges.equalTo(superview.safeAreaLayoutGuide) }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topic/TopicCommentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicCommentCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/19/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicCommentCell: TopicWebCell {
12 |
13 | public var comment: Comment? { didSet { didSetComment() } }
14 | private var createdAtLabel: UILabel!
15 |
16 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
17 | super.init(style: style, reuseIdentifier: reuseIdentifier)
18 |
19 | let stackView = UIStackView()
20 | stackView.axis = .vertical
21 | stackView.spacing = 8
22 | contentView.addSubview(stackView)
23 | stackView.snp.makeConstraints { $0.margins.equalToSuperview().priority(999) }
24 |
25 | createdAtLabel = UILabel()
26 | createdAtLabel.font = .preferredFont(forTextStyle: .subheadline)
27 | createdAtLabel.numberOfLines = 0
28 | createdAtLabel.textColor = .secondaryLabel
29 | stackView.addArrangedSubview(createdAtLabel)
30 |
31 | stackView.addArrangedSubview(webView)
32 | }
33 |
34 | private func didSetComment() {
35 | bodyHTML = comment?.bodyHTML
36 | createdAtLabel.text = "第 \((comment?.index ?? 0) + 1) 条附言 · \(comment?.createdAt ?? "")"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topics/TopicsNodesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicsNodesView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/22/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicsNodesView: UIScrollView {
12 |
13 | public var isEnabled = true { didSet { didSetEnabled() } }
14 | public var nodes: [Node]? { didSet { didSetNodes() } }
15 | private var stackView: UIStackView!
16 |
17 | override init(frame: CGRect) {
18 | super.init(frame: frame)
19 |
20 | alwaysBounceHorizontal = true
21 |
22 | showsHorizontalScrollIndicator = false
23 |
24 | stackView = UIStackView()
25 | stackView.alignment = .center
26 | stackView.spacing = 8
27 | addSubview(stackView)
28 | stackView.snp.makeConstraints { $0.leading.trailing.centerY.equalToSuperview() }
29 |
30 | let topicsNodeButton = TopicsNodeButton()
31 | topicsNodeButton.isSelected = true
32 | topicsNodeButton.node = .all
33 | stackView.addArrangedSubview(topicsNodeButton)
34 | }
35 |
36 | @available(*, unavailable)
37 | required init?(coder aDecoder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | private func didSetEnabled() {
42 | stackView.arrangedSubviews.forEach { ($0 as? TopicsNodeButton)?.isEnabled = isEnabled }
43 | }
44 |
45 | private func didSetNodes() {
46 | nodes?.forEach { node in
47 | let topicsNodeButton = TopicsNodeButton()
48 | topicsNodeButton.node = node
49 | stackView.addArrangedSubview(topicsNodeButton)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/V2EX/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/7/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Firebase
10 | import Regex
11 | import SnapKit
12 | import SwiftDate
13 | import UIButtonSetBackgroundColorForState
14 |
15 | @UIApplicationMain
16 | class AppDelegate: UIResponder, UIApplicationDelegate {
17 |
18 | var window: UIWindow?
19 |
20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
21 |
22 | FirebaseApp.configure()
23 |
24 | SwiftDate.defaultRegion = Region(calendar: Calendars.gregorian, zone: Zones.asiaShanghai, locale: Locales.chinese)
25 |
26 | let tabBarController = UITabBarController()
27 | tabBarController.viewControllers = [
28 | (TopicsController(), UITabBarItem(tabBarSystemItem: .featured, tag: 0)),
29 | (NodesController(), UITabBarItem(tabBarSystemItem: .mostViewed, tag: 1)),
30 | (MoreController(), UITabBarItem(tabBarSystemItem: .more, tag: 2)),
31 | ].map { viewController, tabBarItem in
32 | let navigationController = UINavigationController(rootViewController: viewController)
33 | navigationController.navigationBar.prefersLargeTitles = true
34 | navigationController.tabBarItem = tabBarItem
35 | return navigationController
36 | }
37 | tabBarController.viewControllers?[1].tabBarItem.setValue("节点", forKey: "internalTitle")
38 |
39 | window = UIWindow(frame: UIScreen.main.bounds)
40 | window?.rootViewController = tabBarController
41 | window?.makeKeyAndVisible()
42 |
43 | return true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/SignIn/SignInPasswordCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignInPasswordCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/22/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SignInPasswordCell: UITableViewCell {
12 |
13 | public var passwordField: UITextField!
14 |
15 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
16 | super.init(style: style, reuseIdentifier: reuseIdentifier)
17 |
18 | selectionStyle = .none
19 |
20 | textLabel?.text = " "
21 |
22 | passwordField = UITextField()
23 | passwordField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
24 | passwordField.clearButtonMode = .whileEditing
25 | passwordField.delegate = self
26 | passwordField.font = .preferredFont(forTextStyle: .body)
27 | passwordField.isSecureTextEntry = true
28 | passwordField.placeholder = "密码"
29 | passwordField.returnKeyType = .next
30 | passwordField.textContentType = .password
31 | contentView.addSubview(passwordField)
32 | passwordField.snp.makeConstraints { $0.edges.equalTo(textLabel ?? .init()) }
33 | }
34 |
35 | @available(*, unavailable)
36 | required init?(coder aDecoder: NSCoder) {
37 | fatalError("init(coder:) has not been implemented")
38 | }
39 |
40 | @objc
41 | private func textFieldDidChange(_ textField: UITextField) {
42 | (viewController as? SignInController)?.session?.password = textField.text
43 | }
44 | }
45 |
46 | extension SignInPasswordCell: UITextFieldDelegate {
47 |
48 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
49 | (next(of: UITableView.self)?.cellForRow(at: IndexPath(row: 0, section: 1)) as? SignInCaptchaCell)?.captchaField.becomeFirstResponder()
50 | return false
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/V2EX/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | V2EX
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | ITSAppUsesNonExemptEncryption
24 |
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoadsInWebContent
30 |
31 |
32 | NSFaceIDUsageDescription
33 |
34 | UILaunchStoryboardName
35 | LaunchScreen
36 | UIRequiredDeviceCapabilities
37 |
38 | armv7
39 |
40 | UISupportedInterfaceOrientations
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 | UISupportedInterfaceOrientations~ipad
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationPortraitUpsideDown
50 | UIInterfaceOrientationLandscapeLeft
51 | UIInterfaceOrientationLandscapeRight
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/SignIn/SignInUsernameCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignInUsernameCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/22/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SignInUsernameCell: UITableViewCell {
12 |
13 | public var usernameField: UITextField!
14 |
15 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
16 | super.init(style: style, reuseIdentifier: reuseIdentifier)
17 |
18 | selectionStyle = .none
19 |
20 | textLabel?.text = " "
21 |
22 | usernameField = UITextField()
23 | usernameField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
24 | usernameField.autocapitalizationType = .none
25 | usernameField.autocorrectionType = .no
26 | usernameField.clearButtonMode = .whileEditing
27 | usernameField.delegate = self
28 | usernameField.font = .preferredFont(forTextStyle: .body)
29 | usernameField.placeholder = "帐号"
30 | usernameField.returnKeyType = .next
31 | usernameField.textContentType = .username
32 | contentView.addSubview(usernameField)
33 | usernameField.snp.makeConstraints { $0.edges.equalTo(textLabel ?? .init()) }
34 | }
35 |
36 | @available(*, unavailable)
37 | required init?(coder aDecoder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | @objc
42 | private func textFieldDidChange(_ textField: UITextField) {
43 | (viewController as? SignInController)?.session?.username = textField.text
44 | }
45 | }
46 |
47 | extension SignInUsernameCell: UITextFieldDelegate {
48 |
49 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
50 | (next(of: UITableView.self)?.cellForRow(at: IndexPath(row: 1, section: 0)) as? SignInPasswordCell)?.passwordField.becomeFirstResponder()
51 | return false
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 |
49 | Pods/
50 |
51 | # Add this line if you want to avoid checking in source code from the Xcode workspace
52 | # *.xcworkspace
53 |
54 | # Carthage
55 | #
56 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
57 | # Carthage/Checkouts
58 |
59 | Carthage/Build
60 |
61 | # fastlane
62 | #
63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
64 | # screenshots whenever they are needed.
65 | # For more information about the recommended setup visit:
66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
67 |
68 | fastlane/report.xml
69 | fastlane/Preview.html
70 | fastlane/screenshots/**/*.png
71 | fastlane/test_output
72 |
73 | # Code Injection
74 | #
75 | # After new code Injection tools there's a generated folder /iOSInjectionProject
76 | # https://github.com/johnno1962/injectionforxcode
77 |
78 | iOSInjectionProject/
79 |
80 | GoogleService-Info.plist
81 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topics/TopicsNodeButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicsNodeButton.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/22/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicsNodeButton: UIButton {
12 |
13 | public var node: Node! { didSet { didSetNode() } }
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | addTarget(self, action: #selector(selectNode), for: .touchUpInside)
19 |
20 | clipsToBounds = true
21 |
22 | contentEdgeInsets = UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8)
23 |
24 | layer.cornerRadius = 5
25 |
26 | setBackgroundColor(.clear, for: .normal)
27 |
28 | setTitleColor(.white, for: .selected)
29 | setTitleColor(.white, for: [.selected, .highlighted])
30 | setTitleColor(.white, for: [.selected, .disabled])
31 |
32 | titleLabel?.font = .systemFont(ofSize: 15)
33 |
34 | tintColorDidChange()
35 | }
36 |
37 | @available(*, unavailable)
38 | required init?(coder aDecoder: NSCoder) {
39 | fatalError("init(coder:) has not been implemented")
40 | }
41 |
42 | override func tintColorDidChange() {
43 | super.tintColorDidChange()
44 |
45 | setBackgroundColor(tintColor, for: .selected)
46 | setBackgroundColor(tintColor, for: [.selected, .highlighted])
47 | setBackgroundColor(tintColor.withAlphaComponent(0.16), for: .highlighted)
48 | setBackgroundColor(tintColor.withAlphaComponent(0.5), for: [.selected, .disabled])
49 |
50 | setTitleColor(tintColor, for: .normal)
51 | setTitleColor(tintColor.withAlphaComponent(0.5), for: .disabled)
52 | }
53 |
54 | private func didSetNode() {
55 | setTitle(node.name, for: .normal)
56 | }
57 |
58 | @objc
59 | private func selectNode() {
60 | if isSelected { return }
61 |
62 | (superview as? UIStackView)?.arrangedSubviews.forEach { ($0 as? TopicsNodeButton)?.isSelected = $0 == self }
63 |
64 | (viewController as? TopicsController)?.node = node
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | excluded:
2 | - Pods
3 |
4 | disabled_rules:
5 | - cyclomatic_complexity
6 | - file_length
7 | - function_body_length
8 | - line_length
9 | - nesting
10 | - type_body_length
11 |
12 | opt_in_rules:
13 | - anyobject_protocol
14 | - array_init
15 | - attributes
16 | - closure_end_indentation
17 | - closure_spacing
18 | - collection_alignment
19 | - contains_over_filter_count
20 | - contains_over_filter_is_empty
21 | - contains_over_first_not_nil
22 | - convenience_type
23 | - discouraged_object_literal
24 | - empty_collection_literal
25 | - empty_xctest_method
26 | - explicit_init
27 | - extension_access_modifier
28 | - fallthrough
29 | - fatal_error_message
30 | - file_name
31 | - first_where
32 | - force_unwrapping
33 | - function_default_parameter_at_end
34 | - identical_operands
35 | - implicit_return
36 | - joined_default_parameter
37 | - last_where
38 | - legacy_multiple
39 | - legacy_random
40 | - let_var_whitespace
41 | - literal_expression_end_indentation
42 | - modifier_order
43 | - multiline_arguments
44 | - multiline_function_chains
45 | - multiline_parameters
46 | - nimble_operator
47 | - no_extension_access_modifier
48 | - number_separator
49 | - operator_usage_whitespace
50 | - overridden_super_call
51 | - override_in_extension
52 | - pattern_matching_keywords
53 | - prefixed_toplevel_constant
54 | - private_action
55 | - private_outlet
56 | - prohibited_interface_builder
57 | - quick_discouraged_call
58 | - quick_discouraged_focused_test
59 | - quick_discouraged_pending_test
60 | - reduce_into
61 | - redundant_type_annotation
62 | - required_enum_case
63 | - single_test_class
64 | - sorted_first_last
65 | - sorted_imports
66 | - static_operator
67 | - strict_fileprivate
68 | - strong_iboutlet
69 | - switch_case_on_newline
70 | - trailing_closure
71 | - toggle_bool
72 | - unavailable_function
73 | - unneeded_parentheses_in_closure_argument
74 | - unowned_variable_capture
75 | - untyped_error_in_catch
76 | - unused_import
77 | - unused_private_declaration
78 | - vertical_parameter_alignment_on_call
79 | - vertical_whitespace_between_cases
80 | - vertical_whitespace_closing_braces
81 | - xct_specific_matcher
82 | - yoda_condition
83 |
84 | identifier_name:
85 | excluded:
86 | - id
87 |
88 | trailing_comma:
89 | mandatory_comma: true
90 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/User/UserHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserHeaderView.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/16/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UserHeaderView: UITableViewHeaderFooterView {
12 |
13 | private var avatarView: UIImageView!
14 | private var createdAtLabel: UILabel!
15 | private var idLabel: UILabel!
16 | private var nameLabel: UILabel!
17 | public var user: User? { didSet { didSetUser() } }
18 |
19 | override init(reuseIdentifier: String?) {
20 | super.init(reuseIdentifier: reuseIdentifier)
21 |
22 | let stackView = UIStackView()
23 | stackView.alignment = .center
24 | stackView.axis = .vertical
25 | stackView.spacing = 12
26 | contentView.addSubview(stackView)
27 | stackView.snp.makeConstraints { make in
28 | make.leadingMargin.trailingMargin.equalToSuperview()
29 | make.top.equalToSuperview().offset(20)
30 | make.bottom.equalToSuperview().offset(-20).priority(999)
31 | }
32 |
33 | avatarView = UIImageView()
34 | avatarView.backgroundColor = .secondarySystemGroupedBackground
35 | avatarView.clipsToBounds = true
36 | avatarView.layer.cornerRadius = 44
37 | avatarView.snp.makeConstraints { $0.size.equalTo(88) }
38 | stackView.addArrangedSubview(avatarView)
39 |
40 | nameLabel = UILabel()
41 | nameLabel.font = .preferredFont(forTextStyle: .title1)
42 | stackView.addArrangedSubview(nameLabel)
43 |
44 | idLabel = UILabel()
45 | idLabel.font = .preferredFont(forTextStyle: .body)
46 | stackView.addArrangedSubview(idLabel)
47 |
48 | createdAtLabel = UILabel()
49 | createdAtLabel.font = .preferredFont(forTextStyle: .body)
50 | createdAtLabel.textColor = .secondaryLabel
51 | stackView.addArrangedSubview(createdAtLabel)
52 | }
53 |
54 | @available(*, unavailable)
55 | required init?(coder aDecoder: NSCoder) {
56 | fatalError("init(coder:) has not been implemented")
57 | }
58 |
59 | private func didSetUser() {
60 | avatarView.setImage(withURL: user?.avatar)
61 | createdAtLabel.text = user?.createdAt != nil ? "加入于\(user?.createdAt?.toRelative() ?? "")" : nil
62 | idLabel.text = user?.id != nil ? "第 \(user?.id ?? 0) 号会员" : nil
63 | nameLabel.text = user?.name
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/SignIn/SignInCaptchaCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignInCaptchaCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/17/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SignInCaptchaCell: UITableViewCell {
12 |
13 | public var captchaField: UITextField!
14 | public var captchaImageView: UIImageView!
15 |
16 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
17 | super.init(style: style, reuseIdentifier: reuseIdentifier)
18 |
19 | selectionStyle = .none
20 |
21 | textLabel?.text = " "
22 |
23 | let stackView = UIStackView()
24 | stackView.spacing = 15
25 | contentView.addSubview(stackView)
26 | stackView.snp.makeConstraints { $0.edges.equalTo(textLabel ?? .init()) }
27 |
28 | captchaField = UITextField()
29 | captchaField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
30 | captchaField.autocapitalizationType = .none
31 | captchaField.autocorrectionType = .no
32 | captchaField.clearButtonMode = .whileEditing
33 | captchaField.delegate = self
34 | captchaField.font = .preferredFont(forTextStyle: .body)
35 | captchaField.placeholder = "验证码"
36 | captchaField.returnKeyType = .join
37 | stackView.addArrangedSubview(captchaField)
38 |
39 | captchaImageView = UIImageView()
40 | captchaImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
41 | captchaImageView.isUserInteractionEnabled = true
42 | captchaImageView.snp.makeConstraints { $0.width.equalTo(captchaImageView.snp.height).multipliedBy(176 / 44.0) }
43 | stackView.addArrangedSubview(captchaImageView)
44 | }
45 |
46 | @available(*, unavailable)
47 | required init?(coder aDecoder: NSCoder) {
48 | fatalError("init(coder:) has not been implemented")
49 | }
50 |
51 | @objc
52 | private func textFieldDidChange(_ textField: UITextField) {
53 | (viewController as? SignInController)?.session?.captcha = textField.text
54 | }
55 |
56 | @objc
57 | private func fetchData() {
58 | (viewController as? SignInController)?.fetchData()
59 | }
60 | }
61 |
62 | extension SignInCaptchaCell: UITextFieldDelegate {
63 |
64 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
65 | (viewController as? SignInController)?.signIn()
66 | return false
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/V2EX/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-Small-20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-Small-20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-Small@2x.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-Small@3x.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-Small-40@2x.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-60@2x.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-60@2x.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-60@3x.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "20x20",
53 | "idiom" : "ipad",
54 | "filename" : "Icon-Small-20.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-Small-20@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-Small.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-Small@2x.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-Small-20@2x.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-Small-40@2x.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-76.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-76@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "Icon-1024.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/V2EX.xcodeproj/xcshareddata/xcschemes/V2EX.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import JGProgressHUD
10 |
11 | class ViewController: UIViewController {
12 |
13 | private var progressHUD: JGProgressHUD?
14 |
15 | init() {
16 | super.init(nibName: nil, bundle: nil)
17 |
18 | userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
19 | }
20 |
21 | @available(*, unavailable)
22 | required init?(coder aDecoder: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 |
26 | override func viewWillAppear(_ animated: Bool) {
27 | super.viewWillAppear(animated)
28 |
29 | userActivity?.becomeCurrent()
30 | }
31 |
32 | override func viewDidDisappear(_ animated: Bool) {
33 | super.viewDidDisappear(animated)
34 |
35 | userActivity?.invalidate()
36 | }
37 |
38 | internal var baseURL: URL {
39 | return URL(string: "https://www.v2ex.com") ?? .init(fileURLWithPath: "")
40 | }
41 |
42 | @objc
43 | internal func dismiss(_ sender: Any? = nil) {
44 | view.endEditing(true)
45 | dismiss(animated: true)
46 | }
47 |
48 | internal func hideHUD() {
49 | progressHUD?.dismiss(animated: false)
50 | }
51 |
52 | internal func networkError() {
53 | let alertController = UIAlertController(title: "网络错误", message: nil, preferredStyle: .alert)
54 | alertController.addAction(UIAlertAction(title: "好", style: .default))
55 | present(alertController, animated: true)
56 | }
57 |
58 | internal func showHUD() {
59 | var ancestor: UIViewController = self
60 | while let parent = ancestor.parent { ancestor = parent }
61 | let style: JGProgressHUDStyle = traitCollection.userInterfaceStyle != .dark ? .extraLight : .dark
62 | if progressHUD?.style != style {
63 | progressHUD = JGProgressHUD(style: style)
64 | }
65 | progressHUD?.show(in: ancestor.view, animated: false)
66 | }
67 |
68 | internal func signInRequired(_ sender: Any) {
69 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
70 | alertController.addAction(UIAlertAction(title: "登录", style: .default) { _ in
71 | let tabBarController = self.tabBarController
72 | self.navigationController?.popToRootViewController(animated: false)
73 | tabBarController?.selectedIndex = 0
74 | let navigationController = tabBarController?.selectedViewController as? UINavigationController
75 | navigationController?.popToRootViewController(animated: false)
76 | (navigationController?.viewControllers.first as? TopicsController)?.showSignIn()
77 | })
78 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
79 | alertController.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem
80 | alertController.popoverPresentationController?.sourceRect = (sender as? UIView)?.bounds ?? .zero
81 | alertController.popoverPresentationController?.sourceView = sender as? UIView
82 | present(alertController, animated: true)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/ComposeController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComposeController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 6/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 |
11 | class ComposeController: ViewController {
12 |
13 | public var reply: Reply?
14 | private var textView: UITextView!
15 | public var topic: Topic?
16 |
17 | override init() {
18 | super.init()
19 |
20 | navigationItem.leftBarButtonItem = UIBarButtonItem(title: "放弃", style: .plain, target: self, action: #selector(discard))
21 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
22 | navigationItem.rightBarButtonItem?.isEnabled = false
23 |
24 | title = "回复"
25 | }
26 |
27 | override func loadView() {
28 | textView = UITextView()
29 | textView.alwaysBounceVertical = true
30 | textView.contentInset = UIEdgeInsets(top: 3, left: 15, bottom: 3, right: 15)
31 | textView.delegate = self
32 | textView.font = .preferredFont(forTextStyle: .body)
33 | textView.text = reply != nil ? "@\(reply?.user?.name ?? "") " : nil
34 | view = textView
35 | }
36 |
37 | override func viewWillAppear(_ animated: Bool) {
38 | super.viewWillAppear(animated)
39 |
40 | textView.becomeFirstResponder()
41 | }
42 |
43 | @objc
44 | private func discard() {
45 | view.endEditing(true)
46 | if textView.text == "" { return dismiss(animated: true) }
47 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
48 | alertController.addAction(UIAlertAction(title: "放弃", style: .destructive) { _ in
49 | self.dismiss(animated: true)
50 | })
51 | alertController.addAction(UIAlertAction(title: "继续", style: .cancel))
52 | present(alertController, animated: true)
53 | }
54 |
55 | @objc
56 | private func done() {
57 | view.endEditing(true)
58 | showHUD()
59 | let url = baseURL
60 | .appendingPathComponent("t")
61 | .appendingPathComponent(String(topic?.id ?? 0))
62 | AF.request(
63 | url,
64 | method: .post,
65 | parameters: [
66 | "content": textView.text ?? "",
67 | "once": topic?.once ?? "",
68 | ],
69 | headers: [
70 | "Referer": url.absoluteString,
71 | ]
72 | )
73 | .responseString { response in
74 | if 200..<300 ~= response.response?.statusCode ?? 0 {
75 | let alertController = UIAlertController(title: "已回复", message: nil, preferredStyle: .alert)
76 | alertController.addAction(UIAlertAction(title: "好", style: .default) { _ in
77 | self.dismiss(animated: true)
78 | })
79 | self.present(alertController, animated: true)
80 | } else {
81 | self.networkError()
82 | }
83 | self.hideHUD()
84 | }
85 | }
86 | }
87 |
88 | extension ComposeController: UITextViewDelegate {
89 |
90 | func textViewDidChange(_ textView: UITextView) {
91 | navigationItem.rightBarButtonItem?.isEnabled = textView.text != ""
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/WebViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebViewController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/11/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import JXWebViewController
10 | import TUSafariActivity
11 | import WebKit
12 |
13 | class WebViewController: JXWebViewController {
14 |
15 | private var activityIndicatorView: ActivityIndicatorView!
16 | private var isRefreshing = false { didSet { didSetRefreshing() } }
17 | private var networkErrorView: NetworkErrorView!
18 | public var url: URL?
19 |
20 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
21 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
22 |
23 | navigationItem.largeTitleDisplayMode = .never
24 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(action))
25 | }
26 |
27 | override func loadView() {
28 | super.loadView()
29 |
30 | if title != nil {
31 | webViewKeyValueObservations[\WKWebView.title] = nil
32 | }
33 |
34 | activityIndicatorView = ActivityIndicatorView()
35 | view.addSubview(activityIndicatorView)
36 |
37 | networkErrorView = NetworkErrorView()
38 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: webView, action: #selector(webView.reload)))
39 | view.addSubview(networkErrorView)
40 | }
41 |
42 | override func viewWillAppear(_ animated: Bool) {
43 | super.viewWillAppear(animated)
44 |
45 | if webView.url == nil, let url = url {
46 | webView.load(URLRequest(url: url))
47 | }
48 | }
49 |
50 | private func didSetRefreshing() {
51 | if isRefreshing {
52 | networkErrorView.isHidden = true
53 | activityIndicatorView.startAnimating()
54 | } else {
55 | activityIndicatorView.stopAnimating()
56 | }
57 | }
58 |
59 | @objc
60 | private func action(_ barButtonItem: UIBarButtonItem) {
61 | guard let url = webView.url else { return }
62 | let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: [TUSafariActivity()])
63 | activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
64 | present(activityViewController, animated: true)
65 | }
66 | }
67 |
68 | extension WebViewController {
69 |
70 | func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
71 | isRefreshing = true
72 | }
73 |
74 | override func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
75 | super.webView(webView, didFailProvisionalNavigation: navigation, withError: error)
76 |
77 | isRefreshing = false
78 | networkErrorView.isHidden = false
79 | }
80 |
81 | override func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
82 | super.webView(webView, didFail: navigation, withError: error)
83 |
84 | isRefreshing = false
85 | networkErrorView.isHidden = false
86 | }
87 |
88 | override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
89 | super.webView(webView, didFinish: navigation)
90 |
91 | isRefreshing = false
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/MoreController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoreController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 6/17/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MoreController: ViewController {
12 |
13 | private var tableView: UITableView!
14 |
15 | override init() {
16 | super.init()
17 |
18 | title = "更多"
19 | }
20 |
21 | override func loadView() {
22 | tableView = UITableView(frame: .zero, style: .grouped)
23 | tableView.cellLayoutMarginsFollowReadableWidth = true
24 | tableView.dataSource = self
25 | tableView.delegate = self
26 | view = tableView
27 | }
28 |
29 | override func viewWillAppear(_ animated: Bool) {
30 | super.viewWillAppear(animated)
31 |
32 | tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: animated) }
33 | }
34 | }
35 |
36 | extension MoreController: UITableViewDataSource {
37 |
38 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
39 | return 4
40 | }
41 |
42 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
43 | switch indexPath.row {
44 | case 0:
45 | let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
46 | cell.accessoryType = .disclosureIndicator
47 | cell.detailTextLabel?.text = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String
48 | cell.textLabel?.text = "关于"
49 | return cell
50 |
51 | case 1:
52 | let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
53 | cell.accessoryType = .disclosureIndicator
54 | cell.detailTextLabel?.text = "GitHub"
55 | cell.textLabel?.text = "反馈"
56 | return cell
57 |
58 | case 2:
59 | let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
60 | cell.accessoryType = .disclosureIndicator
61 | let diskUsage = URLCache.shared.currentDiskUsage / 1_024 / 1_024
62 | cell.detailTextLabel?.text = diskUsage > 0 ? "\(diskUsage) MB" : "0"
63 | cell.textLabel?.text = "缓存"
64 | return cell
65 |
66 | case 3:
67 | let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
68 | let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
69 | let version = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? ""
70 | cell.detailTextLabel?.text = "\(shortVersion) (\(version))"
71 | cell.selectionStyle = .none
72 | cell.textLabel?.text = "版本"
73 | return cell
74 |
75 | default:
76 | return .init()
77 | }
78 | }
79 | }
80 |
81 | extension MoreController: UITableViewDelegate {
82 |
83 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
84 | switch indexPath.row {
85 | case 0:
86 | let webViewController = WebViewController()
87 | webViewController.title = "关于"
88 | webViewController.url = baseURL.appendingPathComponent("about")
89 | navigationController?.pushViewController(webViewController, animated: true)
90 |
91 | case 1:
92 | let webViewController = WebViewController()
93 | webViewController.title = "反馈"
94 | webViewController.url = URL(string: "https://github.com/swordray/v2ex-ios/issues")
95 | navigationController?.pushViewController(webViewController, animated: true)
96 |
97 | case 2:
98 | URLCache.shared.removeAllCachedResponses()
99 | tableView.reloadRows(at: [indexPath], with: .automatic)
100 |
101 | default:
102 | break
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/V2EX/zh-Hans.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/NodesController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodesController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/19/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import Kanna
11 |
12 | class NodesController: ViewController {
13 |
14 | private var activityIndicatorView: ActivityIndicatorView!
15 | private var isRefreshing = false { didSet { didSetRefreshing() } }
16 | private var networkErrorView: NetworkErrorView!
17 | private var sections: [Section] = []
18 | private var tableView: UITableView!
19 |
20 | override init() {
21 | super.init()
22 |
23 | title = "节点"
24 | }
25 |
26 | override func loadView() {
27 | tableView = UITableView()
28 | tableView.cellLayoutMarginsFollowReadableWidth = true
29 | tableView.dataSource = self
30 | tableView.delegate = self
31 | tableView.refreshControl = UIRefreshControl()
32 | tableView.refreshControl?.addTarget(self, action: #selector(fetchData), for: .valueChanged)
33 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.description())
34 | tableView.tableFooterView = UIView()
35 | view = tableView
36 |
37 | activityIndicatorView = ActivityIndicatorView()
38 | view.addSubview(activityIndicatorView)
39 |
40 | networkErrorView = NetworkErrorView()
41 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
42 | view.addSubview(networkErrorView)
43 | }
44 |
45 | override func viewWillAppear(_ animated: Bool) {
46 | super.viewWillAppear(animated)
47 |
48 | tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: animated) }
49 |
50 | if sections.count == 0 || !networkErrorView.isHidden { fetchData() }
51 | }
52 |
53 | @objc
54 | private func fetchData() {
55 | if isRefreshing { return }
56 | isRefreshing = true
57 | AF.request(
58 | baseURL
59 | )
60 | .responseString { response in
61 | if 200..<300 ~= response.response?.statusCode ?? 0 {
62 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
63 | let json = doc?.css("#Main .box:nth-child(4) .cell:not(:first-child)").map {
64 | [
65 | "name": $0.at_css(".fade")?.text,
66 | "nodes": $0.css("a").map {
67 | [
68 | "name": $0.text,
69 | "code": "[^/]+$".r?.findFirst(in: $0["href"] ?? "")?.matched,
70 | ]
71 | },
72 | ] as [String: Any?]
73 | }
74 | self.sections = (try? [Section](json: json ?? [])) ?? []
75 | self.tableView.reloadData()
76 | } else {
77 | self.networkErrorView.isHidden = false
78 | }
79 | self.isRefreshing = false
80 | }
81 | }
82 |
83 | private func didSetRefreshing() {
84 | if isRefreshing {
85 | networkErrorView.isHidden = true
86 | if tableView.refreshControl?.isRefreshing ?? false { return }
87 | activityIndicatorView.startAnimating()
88 | } else {
89 | tableView.refreshControl?.endRefreshing()
90 | activityIndicatorView.stopAnimating()
91 | }
92 | }
93 | }
94 |
95 | extension NodesController: UITableViewDataSource {
96 |
97 | func numberOfSections(in tableView: UITableView) -> Int {
98 | return sections.count
99 | }
100 |
101 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
102 | return sections[section].nodes.count
103 | }
104 |
105 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
106 | return sections[section].name
107 | }
108 |
109 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
110 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
111 | cell.accessoryType = .disclosureIndicator
112 | cell.textLabel?.text = sections[indexPath.section].nodes[indexPath.row].name
113 | return cell
114 | }
115 | }
116 |
117 | extension NodesController: UITableViewDelegate {
118 |
119 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
120 | let topicsController = TopicsController()
121 | topicsController.node = sections[indexPath.section].nodes[indexPath.row]
122 | navigationController?.pushViewController(topicsController, animated: true)
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topics/TopicsCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicsCell: UITableViewCell {
12 |
13 | private var nameLabel: UILabel!
14 | private var nodeButton: UIButton!
15 | private var repliedAtLabel: UILabel!
16 | private var repliesCountLabel: UILabel!
17 | public var tableViewStyle: UITableView.Style?
18 | public var topic: Topic? { didSet { didSetTopic() } }
19 | private var userAvatarView: UIImageView!
20 | private var userNameLabel: UILabel!
21 |
22 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
23 | super.init(style: style, reuseIdentifier: reuseIdentifier)
24 |
25 | accessoryType = .disclosureIndicator
26 |
27 | let stackView = UIStackView()
28 | stackView.alignment = .center
29 | stackView.spacing = 8
30 | contentView.addSubview(stackView)
31 | stackView.snp.makeConstraints { $0.margins.equalToSuperview().priority(999) }
32 |
33 | userAvatarView = UIImageView()
34 | userAvatarView.backgroundColor = .secondarySystemBackground
35 | userAvatarView.clipsToBounds = true
36 | userAvatarView.layer.cornerRadius = 22
37 | userAvatarView.snp.makeConstraints { $0.size.equalTo(44) }
38 | stackView.addArrangedSubview(userAvatarView)
39 | stackView.setCustomSpacing(15, after: userAvatarView)
40 |
41 | let contentStackView = UIStackView()
42 | contentStackView.axis = .vertical
43 | contentStackView.spacing = 8
44 | contentStackView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
45 | contentStackView.setContentHuggingPriority(.defaultLow, for: .horizontal)
46 | stackView.addArrangedSubview(contentStackView)
47 |
48 | nameLabel = UILabel()
49 | nameLabel.font = .preferredFont(forTextStyle: .body)
50 | nameLabel.numberOfLines = 3
51 | contentStackView.addArrangedSubview(nameLabel)
52 |
53 | let detailStackView = UIStackView()
54 | detailStackView.spacing = 8
55 | contentStackView.addArrangedSubview(detailStackView)
56 |
57 | nodeButton = UIButton()
58 | nodeButton.backgroundColor = .quaternarySystemFill
59 | nodeButton.clipsToBounds = true
60 | nodeButton.contentEdgeInsets = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)
61 | nodeButton.isUserInteractionEnabled = false
62 | nodeButton.layer.cornerRadius = 3
63 | nodeButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
64 | nodeButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
65 | nodeButton.setTitleColor(.secondaryLabel, for: .normal)
66 | nodeButton.titleLabel?.font = .preferredFont(forTextStyle: .subheadline)
67 | detailStackView.addArrangedSubview(nodeButton)
68 |
69 | userNameLabel = UILabel()
70 | userNameLabel.font = .preferredFont(forTextStyle: .subheadline)
71 | userNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
72 | userNameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
73 | userNameLabel.textColor = .secondaryLabel
74 | detailStackView.addArrangedSubview(userNameLabel)
75 |
76 | repliedAtLabel = UILabel()
77 | repliedAtLabel.font = .preferredFont(forTextStyle: .subheadline)
78 | repliedAtLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
79 | repliedAtLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
80 | repliedAtLabel.textColor = .tertiaryLabel
81 | detailStackView.addArrangedSubview(repliedAtLabel)
82 |
83 | repliesCountLabel = UILabel()
84 | repliesCountLabel.font = .preferredFont(forTextStyle: .body)
85 | repliesCountLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
86 | repliesCountLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
87 | repliesCountLabel.textColor = .secondaryLabel
88 | stackView.addArrangedSubview(repliesCountLabel)
89 | }
90 |
91 | @available(*, unavailable)
92 | required init?(coder aDecoder: NSCoder) {
93 | fatalError("init(coder:) has not been implemented")
94 | }
95 |
96 | private func didSetTopic() {
97 | backgroundColor = topic?.isSticky ?? false ? .secondarySystemBackground : tableViewStyle == .plain ? .systemBackground : .secondarySystemGroupedBackground
98 | nameLabel.text = topic?.name
99 | nodeButton.isHidden = topic?.node?.name == nil
100 | nodeButton.setTitle(topic?.node?.name, for: .normal)
101 | repliedAtLabel.text = topic?.repliedAt
102 | repliesCountLabel.text = String(topic?.repliesCount ?? 0)
103 | userAvatarView.isHidden = topic?.user == nil
104 | userAvatarView.setImage(withURL: topic?.user?.avatar)
105 | userNameLabel.isHidden = topic?.user == nil
106 | userNameLabel.text = topic?.user?.name
107 |
108 | if topic?.user != nil {
109 | let size = CGSize(width: 44, height: 1)
110 | UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
111 | let image = UIGraphicsGetImageFromCurrentImageContext()
112 | UIGraphicsEndImageContext()
113 | imageView?.image = image
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topic/TopicNameCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NameCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/9/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicNameCell: UITableViewCell {
12 |
13 | private var createdAtLabel: UILabel!
14 | private var nameLabel: UILabel!
15 | private var nodeButton: UIButton!
16 | public var topic: Topic? { didSet { didSetTopic() } }
17 | private var userAvatarView: UIImageView!
18 | private var userNameButton: UIButton!
19 |
20 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
21 | super.init(style: style, reuseIdentifier: reuseIdentifier)
22 |
23 | selectionStyle = .none
24 |
25 | let stackView = UIStackView()
26 | stackView.axis = .vertical
27 | stackView.spacing = 8
28 | contentView.addSubview(stackView)
29 | stackView.snp.makeConstraints { $0.margins.equalToSuperview().priority(999) }
30 |
31 | nameLabel = UILabel()
32 | nameLabel.font = .preferredFont(forTextStyle: .title2)
33 | nameLabel.numberOfLines = 0
34 | stackView.addArrangedSubview(nameLabel)
35 |
36 | let detailStackView = UIStackView()
37 | detailStackView.alignment = .center
38 | detailStackView.spacing = 8
39 | stackView.addArrangedSubview(detailStackView)
40 |
41 | userAvatarView = UIImageView()
42 | userAvatarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showUser)))
43 | userAvatarView.backgroundColor = .secondarySystemBackground
44 | userAvatarView.clipsToBounds = true
45 | userAvatarView.isUserInteractionEnabled = true
46 | userAvatarView.layer.cornerRadius = 22
47 | userAvatarView.snp.makeConstraints { $0.size.equalTo(44) }
48 | detailStackView.addArrangedSubview(userAvatarView)
49 | detailStackView.setCustomSpacing(15, after: userAvatarView)
50 |
51 | userNameButton = UIButton()
52 | userNameButton.addTarget(self, action: #selector(showUser), for: .touchUpInside)
53 | userNameButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .leastNormalMagnitude)
54 | userNameButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
55 | userNameButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
56 | userNameButton.setTitleColor(tintColor, for: .normal)
57 | userNameButton.setTitleColor(tintColor.withAlphaComponent(0.2), for: .highlighted)
58 | userNameButton.titleLabel?.font = .preferredFont(forTextStyle: .body)
59 | detailStackView.addArrangedSubview(userNameButton)
60 |
61 | let flexibleSpaceView = UIView()
62 | detailStackView.addArrangedSubview(flexibleSpaceView)
63 | detailStackView.setCustomSpacing(0, after: flexibleSpaceView)
64 |
65 | nodeButton = UIButton()
66 | nodeButton.addTarget(self, action: #selector(showNode), for: .touchUpInside)
67 | nodeButton.backgroundColor = .quaternarySystemFill
68 | nodeButton.clipsToBounds = true
69 | nodeButton.contentEdgeInsets = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)
70 | nodeButton.layer.cornerRadius = 3
71 | nodeButton.titleLabel?.font = .preferredFont(forTextStyle: .body)
72 | nodeButton.setTitleColor(tintColor, for: .normal)
73 | nodeButton.setTitleColor(tintColor.withAlphaComponent(0.2), for: .highlighted)
74 | detailStackView.addArrangedSubview(nodeButton)
75 |
76 | createdAtLabel = UILabel()
77 | createdAtLabel.font = .preferredFont(forTextStyle: .subheadline)
78 | createdAtLabel.numberOfLines = 0
79 | createdAtLabel.textColor = .secondaryLabel
80 | stackView.addArrangedSubview(createdAtLabel)
81 | }
82 |
83 | @available(*, unavailable)
84 | required init?(coder aDecoder: NSCoder) {
85 | fatalError("init(coder:) has not been implemented")
86 | }
87 |
88 | override func tintColorDidChange() {
89 | super.tintColorDidChange()
90 |
91 | userNameButton.setTitleColor(tintColor, for: .normal)
92 | nodeButton.setTitleColor(tintColor, for: .normal)
93 | }
94 |
95 | private func didSetTopic() {
96 | createdAtLabel.text = [
97 | topic?.createdAt,
98 | topic?.clicksCount != nil ? "\(topic?.clicksCount ?? 0) 次点击" : nil,
99 | topic?.favoritesCount != nil ? "\(topic?.favoritesCount ?? 0) 人收藏" : nil,
100 | ].compactMap { $0 }.joined(separator: " · ") + " "
101 | nameLabel.text = topic?.name ?? " "
102 | nodeButton.isHidden = topic?.node?.name == nil
103 | nodeButton.setTitle(topic?.node?.name, for: .normal)
104 | userAvatarView.setImage(withURL: topic?.user?.avatar)
105 | userNameButton.setTitle(topic?.user?.name, for: .normal)
106 | }
107 |
108 | @objc
109 | private func showUser() {
110 | guard let user = topic?.user else { return }
111 | let userController = UserController()
112 | userController.user = user
113 | viewController?.navigationController?.pushViewController(userController, animated: true)
114 | }
115 |
116 | @objc
117 | private func showNode() {
118 | guard let node = topic?.node else { return }
119 | let topicsController = TopicsController()
120 | topicsController.node = node
121 | viewController?.navigationController?.pushViewController(topicsController, animated: true)
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Alamofire (5.0.0-rc.2)
3 | - AlamofireImage (4.0.0-beta.5):
4 | - Alamofire (~> 5.0.0-rc.2)
5 | - CrossroadRegex (1.1.0)
6 | - Firebase/Core (6.9.0):
7 | - Firebase/CoreOnly
8 | - FirebaseAnalytics (= 6.1.2)
9 | - Firebase/CoreOnly (6.9.0):
10 | - FirebaseCore (= 6.3.0)
11 | - FirebaseAnalytics (6.1.2):
12 | - FirebaseCore (~> 6.3)
13 | - FirebaseInstanceID (~> 4.2)
14 | - GoogleAppMeasurement (= 6.1.2)
15 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
16 | - GoogleUtilities/MethodSwizzler (~> 6.0)
17 | - GoogleUtilities/Network (~> 6.0)
18 | - "GoogleUtilities/NSData+zlib (~> 6.0)"
19 | - nanopb (~> 0.3)
20 | - FirebaseCore (6.3.0):
21 | - FirebaseCoreDiagnostics (~> 1.0)
22 | - FirebaseCoreDiagnosticsInterop (~> 1.0)
23 | - GoogleUtilities/Environment (~> 6.2)
24 | - GoogleUtilities/Logger (~> 6.2)
25 | - FirebaseCoreDiagnostics (1.1.0):
26 | - FirebaseCoreDiagnosticsInterop (~> 1.0)
27 | - GoogleDataTransportCCTSupport (~> 1.0)
28 | - GoogleUtilities/Environment (~> 6.2)
29 | - GoogleUtilities/Logger (~> 6.2)
30 | - FirebaseCoreDiagnosticsInterop (1.0.0)
31 | - FirebaseInstanceID (4.2.5):
32 | - FirebaseCore (~> 6.0)
33 | - GoogleUtilities/Environment (~> 6.0)
34 | - GoogleUtilities/UserDefaults (~> 6.0)
35 | - GoogleAppMeasurement (6.1.2):
36 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
37 | - GoogleUtilities/MethodSwizzler (~> 6.0)
38 | - GoogleUtilities/Network (~> 6.0)
39 | - "GoogleUtilities/NSData+zlib (~> 6.0)"
40 | - nanopb (~> 0.3)
41 | - GoogleDataTransport (2.0.0)
42 | - GoogleDataTransportCCTSupport (1.1.0):
43 | - GoogleDataTransport (~> 2.0)
44 | - nanopb
45 | - GoogleUtilities/AppDelegateSwizzler (6.3.1):
46 | - GoogleUtilities/Environment
47 | - GoogleUtilities/Logger
48 | - GoogleUtilities/Network
49 | - GoogleUtilities/Environment (6.3.1)
50 | - GoogleUtilities/Logger (6.3.1):
51 | - GoogleUtilities/Environment
52 | - GoogleUtilities/MethodSwizzler (6.3.1):
53 | - GoogleUtilities/Logger
54 | - GoogleUtilities/Network (6.3.1):
55 | - GoogleUtilities/Logger
56 | - "GoogleUtilities/NSData+zlib"
57 | - GoogleUtilities/Reachability
58 | - "GoogleUtilities/NSData+zlib (6.3.1)"
59 | - GoogleUtilities/Reachability (6.3.1):
60 | - GoogleUtilities/Logger
61 | - GoogleUtilities/UserDefaults (6.3.1):
62 | - GoogleUtilities/Logger
63 | - JGProgressHUD (2.0.4)
64 | - JXWebViewController (1.2.1)
65 | - Kanna (5.0.0)
66 | - KeychainAccess (3.2.0)
67 | - Ladybug (2.0.0)
68 | - nanopb (0.3.901):
69 | - nanopb/decode (= 0.3.901)
70 | - nanopb/encode (= 0.3.901)
71 | - nanopb/decode (0.3.901)
72 | - nanopb/encode (0.3.901)
73 | - SnapKit (5.0.1)
74 | - SwiftDate (6.1.0)
75 | - SwiftLint (0.35.0)
76 | - TUSafariActivity (1.0.4)
77 | - UIButtonSetBackgroundColorForState (0.1.0)
78 |
79 | DEPENDENCIES:
80 | - Alamofire (~> 5.0.0-rc.2)
81 | - AlamofireImage (~> 4.0.0-beta.5)
82 | - CrossroadRegex
83 | - Firebase/Core
84 | - JGProgressHUD
85 | - JXWebViewController
86 | - Kanna
87 | - KeychainAccess
88 | - Ladybug
89 | - SnapKit
90 | - SwiftDate
91 | - SwiftLint
92 | - TUSafariActivity
93 | - UIButtonSetBackgroundColorForState
94 |
95 | SPEC REPOS:
96 | trunk:
97 | - Alamofire
98 | - AlamofireImage
99 | - CrossroadRegex
100 | - Firebase
101 | - FirebaseAnalytics
102 | - FirebaseCore
103 | - FirebaseCoreDiagnostics
104 | - FirebaseCoreDiagnosticsInterop
105 | - FirebaseInstanceID
106 | - GoogleAppMeasurement
107 | - GoogleDataTransport
108 | - GoogleDataTransportCCTSupport
109 | - GoogleUtilities
110 | - JGProgressHUD
111 | - JXWebViewController
112 | - Kanna
113 | - KeychainAccess
114 | - Ladybug
115 | - nanopb
116 | - SnapKit
117 | - SwiftDate
118 | - SwiftLint
119 | - TUSafariActivity
120 | - UIButtonSetBackgroundColorForState
121 |
122 | SPEC CHECKSUMS:
123 | Alamofire: f9450d3c7f6bea2ad62e7a541c3e9b186c7991d6
124 | AlamofireImage: f6dfe201f6d59cc6a045b5cb228b58962b8ef31d
125 | CrossroadRegex: 5eaadb3f7b4014728f1cbd1014756b126c1dbd9a
126 | Firebase: 2d750c54cda57d5a6ae31212cfe5cc813c6be7e4
127 | FirebaseAnalytics: 5d9ccbf46ed25d3ec9304d263f85bddf1e93e2d2
128 | FirebaseCore: 8b2765c445d40db7137989b7146a3aa3f91b5529
129 | FirebaseCoreDiagnostics: be4f7a09d02ab305f18de59a470412caddb64c2a
130 | FirebaseCoreDiagnosticsInterop: 6829da2b8d1fc795ff1bd99df751d3788035d2cb
131 | FirebaseInstanceID: 550df9be1f99f751d8fcde3ac342a1e21a0e6c42
132 | GoogleAppMeasurement: 0ae90be1cc4dad40f4a27fc767ef59fa032ec87b
133 | GoogleDataTransport: c8617c00e4f3eb9418e42ac0e8ac5241a9d555dd
134 | GoogleDataTransportCCTSupport: 9f352523e8785a71f6754f51eeff09f49ec19268
135 | GoogleUtilities: f895fde57977df4e0233edda0dbeac490e3703b6
136 | JGProgressHUD: 62658b14e72cccf179efc7a13bcb54d30b92fc22
137 | JXWebViewController: 939b9b9b104e53810e73dc51535294ded7c8abf0
138 | Kanna: a32875df62975f8f4b871e933b4ea1a5c3b8be0f
139 | KeychainAccess: 3b1bf8a77eb4c6ea1ce9404c292e48f948954c6b
140 | Ladybug: f445480856a4971ef401abcb14ff8ea2d1178785
141 | nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
142 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
143 | SwiftDate: fa2bb3962056bb44047b4b85a30044e5eae30b03
144 | SwiftLint: 5553187048b900c91aa03552807681bb6b027846
145 | TUSafariActivity: afc55a00965377939107ce4fdc7f951f62454546
146 | UIButtonSetBackgroundColorForState: fcc963a30f972e15434bacebefe042033940a438
147 |
148 | PODFILE CHECKSUM: fa691ca4f17477d264259c79840ed1e6e7068b9a
149 |
150 | COCOAPODS: 1.10.0.rc.1
151 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topic/TopicReplyCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicReplyCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/10/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicReplyCell: TopicWebCell {
12 |
13 | private var createdAtLabel: UILabel!
14 | private var indexButton: UIButton!
15 | public var reply: Reply? { didSet { didSetReply() } }
16 | private var userAvatarView: UIImageView!
17 | private var userNameButton: UIButton!
18 |
19 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
20 | super.init(style: style, reuseIdentifier: reuseIdentifier)
21 |
22 | let size = CGSize(width: 44, height: 1)
23 | UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
24 | let image = UIGraphicsGetImageFromCurrentImageContext()
25 | UIGraphicsEndImageContext()
26 | imageView?.image = image
27 |
28 | userAvatarView = UIImageView()
29 | userAvatarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showUser)))
30 | userAvatarView.backgroundColor = .secondarySystemBackground
31 | userAvatarView.clipsToBounds = true
32 | userAvatarView.isUserInteractionEnabled = true
33 | userAvatarView.layer.cornerRadius = 22
34 | contentView.addSubview(userAvatarView)
35 | userAvatarView.snp.makeConstraints { make in
36 | make.leading.top.equalTo(contentView.layoutMarginsGuide)
37 | make.bottom.lessThanOrEqualTo(contentView.layoutMarginsGuide).priority(999)
38 | make.size.equalTo(44)
39 | }
40 |
41 | let stackView = UIStackView()
42 | stackView.axis = .vertical
43 | stackView.spacing = 8
44 | contentView.addSubview(stackView)
45 | stackView.snp.makeConstraints { make in
46 | make.trailing.top.equalTo(contentView.layoutMarginsGuide)
47 | make.bottom.lessThanOrEqualTo(contentView.layoutMarginsGuide)
48 | make.leading.equalTo(userAvatarView.snp.trailing).offset(15)
49 | }
50 |
51 | let detailStackView = UIStackView()
52 | detailStackView.spacing = 8
53 | stackView.addArrangedSubview(detailStackView)
54 |
55 | userNameButton = UIButton()
56 | userNameButton.addTarget(self, action: #selector(showUser), for: .touchUpInside)
57 | userNameButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .leastNormalMagnitude)
58 | userNameButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
59 | userNameButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
60 | userNameButton.setTitleColor(tintColor, for: .normal)
61 | userNameButton.setTitleColor(tintColor.withAlphaComponent(0.2), for: .highlighted)
62 | userNameButton.titleLabel?.font = .preferredFont(forTextStyle: .body)
63 | detailStackView.addArrangedSubview(userNameButton)
64 |
65 | createdAtLabel = UILabel()
66 | createdAtLabel.font = .preferredFont(forTextStyle: .subheadline)
67 | createdAtLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
68 | createdAtLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
69 | createdAtLabel.textColor = .secondaryLabel
70 | detailStackView.addArrangedSubview(createdAtLabel)
71 |
72 | indexButton = UIButton()
73 | indexButton.addTarget(self, action: #selector(showActions), for: .touchUpInside)
74 | indexButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .leastNormalMagnitude)
75 | indexButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
76 | indexButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
77 | indexButton.setTitleColor(tintColor, for: .normal)
78 | indexButton.setTitleColor(tintColor.withAlphaComponent(0.2), for: .highlighted)
79 | indexButton.titleLabel?.font = .preferredFont(forTextStyle: .body)
80 | detailStackView.addArrangedSubview(indexButton)
81 |
82 | stackView.addArrangedSubview(webView)
83 | }
84 |
85 | override func tintColorDidChange() {
86 | super.tintColorDidChange()
87 |
88 | userNameButton.setTitleColor(tintColor, for: .normal)
89 | indexButton.setTitleColor(tintColor, for: .normal)
90 | }
91 |
92 | private func didSetReply() {
93 | bodyHTML = reply?.bodyHTML
94 | createdAtLabel.text = reply?.createdAt
95 | indexButton.setTitle("#\((reply?.index ?? 0) + 1)", for: .normal)
96 | userAvatarView.setImage(withURL: reply?.user?.avatar)
97 | userNameButton.setTitle(reply?.user?.name, for: .normal)
98 | }
99 |
100 | @objc
101 | private func showUser() {
102 | let userController = UserController()
103 | userController.user = reply?.user
104 | viewController?.navigationController?.pushViewController(userController, animated: true)
105 | }
106 |
107 | @objc
108 | private func showActions(_ button: UIButton) {
109 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
110 | alertController.addAction(UIAlertAction(title: "回复", style: .default) { _ in
111 | (self.viewController as? TopicController)?.showCompose(button, self.reply)
112 | })
113 | alertController.addAction(UIAlertAction(title: "举报", style: .destructive) { _ in
114 | (self.viewController as? TopicController)?.report()
115 | })
116 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
117 | alertController.popoverPresentationController?.sourceRect = button.bounds
118 | alertController.popoverPresentationController?.sourceView = button
119 | viewController?.present(alertController, animated: true)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/V2EX/Classes/Views/Topic/TopicWebCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicWebCell.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/10/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WebKit
11 |
12 | class TopicWebCell: UITableViewCell {
13 |
14 | public var bodyHTML: String? { didSet { didSetBodyHTML() } }
15 | public var webView: UIWebView!
16 | private var webViewObservation: NSKeyValueObservation?
17 |
18 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
19 | super.init(style: style, reuseIdentifier: reuseIdentifier)
20 |
21 | selectionStyle = .none
22 |
23 | webView = UIWebView()
24 | webView.backgroundColor = .clear
25 | webView.dataDetectorTypes = []
26 | webView.delegate = self
27 | webView.isOpaque = false
28 | webView.scrollView.isScrollEnabled = false
29 | webView.snp.makeConstraints { $0.height.equalTo(Int(UIFont.preferredFont(forTextStyle: .body).pointSize)).priority(999) }
30 |
31 | webViewObservation = webView.observe(\.scrollView.contentSize) { webView, _ in
32 | webView.snp.updateConstraints { $0.height.equalTo(webView.scrollView.contentSize.height).priority(999) }
33 | let tableView = self.next(of: UITableView.self)
34 | guard let indexPath = tableView?.indexPath(for: self) else { return }
35 | tableView?.reloadRows(at: [indexPath], with: .none)
36 | }
37 | }
38 |
39 | @available(*, unavailable)
40 | required init?(coder aDecoder: NSCoder) {
41 | fatalError("init(coder:) has not been implemented")
42 | }
43 |
44 | private func didSetBodyHTML() {
45 | guard let htmlString = htmlString, webView.request == nil else { return }
46 | webView.loadHTMLString(htmlString, baseURL: viewController?.baseURL)
47 | }
48 |
49 | private var htmlString: String? {
50 | guard let bodyHTML = bodyHTML else { return nil }
51 | let style = """
52 | *, *::before, *::after { box-sizing: border-box; padding: 0; margin: 0; }
53 | :root { color-scheme: light dark; }
54 | html, body { width: 100%; height: 100%; font-family: -apple-system; font: -apple-system-body; word-wrap: break-word; -webkit-tap-highlight-color: transparent; }
55 | h1, h2, h3, h4, h5, h6 { margin: 1.5em 0 1em; font: -apple-system-title3; }
56 | ol, ul { padding-left: 1.5em; margin: 1em 0; }
57 | ol { list-style-type: decimal; }
58 | ul { list-style-type: disc; }
59 | li { margin: 0.5em 0; }
60 | hr { margin: 1em 0; border: none; border-bottom: 0.5px solid #c6c6c8; }
61 | @media (prefers-color-scheme: dark) { hr { border-bottom-color: #38383a; } }
62 | pre { padding: 0.5em; margin: 1em 0; font: -apple-system-footnote; white-space: pre-wrap; background-color: #f2f2f7; }
63 | @media (prefers-color-scheme: dark) { pre { background-color: #1c1c1e; } }
64 | p { margin: 0.5em 0; }
65 | a { color: #007aff; text-decoration: none; }
66 | @media (prefers-color-scheme: dark) { a { color: #0a84ff; } }
67 | img { max-width: 100%; }
68 | button { display: none; }
69 | body :first-child { margin-top: 0; }
70 | body :last-child { margin-bottom: 0; }
71 | """
72 | let script = """
73 | document.addEventListener('DOMContentLoaded', function(event) {
74 | var images = document.querySelectorAll('img:not(.emoji):not(.twemoji)')
75 | for (var i = 0; i < images.length; i++) {
76 | var image = images[i]
77 | var a = document.createElement('a')
78 | a.href = image.src + '#imageview'
79 | image.parentElement.insertBefore(a, image)
80 | a.appendChild(image)
81 | }
82 | })
83 | """
84 | return """
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | \(bodyHTML)
93 |
94 | """
95 | }
96 | }
97 |
98 | extension TopicWebCell: UIWebViewDelegate {
99 |
100 | func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
101 | guard navigationType == .linkClicked, let url = request.url else { return true }
102 | if url.scheme == "applewebdata" || url.host == "www.v2ex.com" {
103 | if let name = "^/member/([^/]+)$".r?.findFirst(in: url.path)?.group(at: 1) {
104 | let userController = UserController()
105 | userController.user = try? User(json: ["name": name])
106 | viewController?.navigationController?.pushViewController(userController, animated: true)
107 | } else if let id = "^/t/(\\d+)$".r?.findFirst(in: url.path)?.group(at: 1) {
108 | let topicController = TopicController()
109 | topicController.topic = try? Topic(json: ["id": Int(id)])
110 | viewController?.navigationController?.pushViewController(topicController, animated: true)
111 | } else {
112 | let webViewController = WebViewController()
113 | webViewController.url = viewController?.baseURL.appendingPathComponent(url.path)
114 | viewController?.navigationController?.pushViewController(webViewController, animated: true)
115 | }
116 | } else if WKWebView.handlesURLScheme(url.scheme ?? "") {
117 | let webViewController = WebViewController()
118 | webViewController.title = url.fragment == "imageview" ? "图片" : nil
119 | webViewController.url = url
120 | viewController?.navigationController?.pushViewController(webViewController, animated: true)
121 | } else if UIApplication.shared.canOpenURL(url) {
122 | UIApplication.shared.open(url)
123 | }
124 | return false
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/SignInController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignInController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/17/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import Kanna
11 | import KeychainAccess
12 |
13 | class SignInController: ViewController {
14 |
15 | private var activityIndicatorView: ActivityIndicatorView!
16 | private var isRefreshing = false { didSet { didSetRefreshing() } }
17 | private var networkErrorView: NetworkErrorView!
18 | public var session: Session?
19 | private var tableView: UITableView!
20 | private var termsSwitch: UISwitch!
21 |
22 | override init() {
23 | super.init()
24 |
25 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismiss))
26 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(action))
27 |
28 | title = "登录"
29 | }
30 |
31 | override func loadView() {
32 | tableView = UITableView(frame: .zero, style: .grouped)
33 | tableView.cellLayoutMarginsFollowReadableWidth = true
34 | tableView.dataSource = self
35 | tableView.delegate = self
36 | tableView.register(SignInCaptchaCell.self, forCellReuseIdentifier: SignInCaptchaCell.description())
37 | tableView.register(SignInPasswordCell.self, forCellReuseIdentifier: SignInPasswordCell.description())
38 | tableView.register(SignInUsernameCell.self, forCellReuseIdentifier: SignInUsernameCell.description())
39 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.description())
40 | view = tableView
41 |
42 | activityIndicatorView = ActivityIndicatorView()
43 | view.addSubview(activityIndicatorView)
44 |
45 | networkErrorView = NetworkErrorView()
46 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
47 | view.addSubview(networkErrorView)
48 |
49 | termsSwitch = UISwitch()
50 | termsSwitch.addTarget(tableView, action: #selector(tableView.reloadData), for: .valueChanged)
51 | termsSwitch.isOn = true
52 | }
53 |
54 | override func viewWillAppear(_ animated: Bool) {
55 | super.viewWillAppear(animated)
56 |
57 | tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: animated) }
58 |
59 | fetchData()
60 | }
61 |
62 | @objc
63 | internal func fetchData() {
64 | if isRefreshing { return }
65 | isRefreshing = true
66 | (tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? SignInCaptchaCell)?.captchaField.text = nil
67 | AF.request(
68 | baseURL.appendingPathComponent("signin")
69 | ).responseString { response in
70 | if 200..<300 ~= response.response?.statusCode ?? 0 {
71 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
72 | if let message = doc?.at_css(".topic_content p")?.text {
73 | self.view.endEditing(true)
74 | let alertController = UIAlertController(title: "登录受限", message: message, preferredStyle: .alert)
75 | alertController.addAction(UIAlertAction(title: "好", style: .default) { _ in
76 | self.dismiss(animated: true)
77 | })
78 | self.present(alertController, animated: true)
79 | } else {
80 | let json = [
81 | "usernameKey": doc?.at_css("#Main form tr:nth-child(1) input")?["name"],
82 | "passwordKey": doc?.at_css("#Main form tr:nth-child(2) input")?["name"],
83 | "captchaKey": doc?.at_css("#Main form tr:nth-child(3) input")?["name"],
84 | "once": "once=(\\d+)".r?.findFirst(in: doc?.at_css("#Main form tr:nth-child(3) td:nth-child(2) div[style]")?["style"] ?? "")?.group(at: 1),
85 | "username": self.session?.username,
86 | "password": self.session?.password,
87 | ]
88 | self.session = try? Session(json: json)
89 | self.tableView.reloadSections([1], with: .none)
90 | }
91 | } else {
92 | self.networkErrorView.isHidden = false
93 | }
94 | self.isRefreshing = false
95 | }
96 | }
97 |
98 | private func didSetRefreshing() {
99 | if isRefreshing {
100 | networkErrorView.isHidden = true
101 | activityIndicatorView.startAnimating()
102 | } else {
103 | activityIndicatorView.stopAnimating()
104 | }
105 | }
106 |
107 | @objc
108 | private func action() {
109 | view.endEditing(true)
110 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
111 | [("注册", "signup"), ("忘记密码", "forgot")].forEach { title, pathComponent in
112 | alertController.addAction(UIAlertAction(title: title, style: .default) { _ in
113 | let webViewController = WebViewController()
114 | webViewController.title = title
115 | webViewController.url = self.baseURL.appendingPathComponent(pathComponent)
116 | self.navigationController?.pushViewController(webViewController, animated: true)
117 | })
118 | }
119 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
120 | present(alertController, animated: true)
121 | }
122 |
123 | internal func signIn() {
124 | guard let session = session else { return }
125 | view.endEditing(true)
126 | showHUD()
127 | AF.request(
128 | baseURL.appendingPathComponent("signin"),
129 | method: .post,
130 | parameters: [
131 | session.usernameKey: session.username ?? "",
132 | session.passwordKey: session.password ?? "",
133 | session.captchaKey: session.captcha ?? "",
134 | "once": session.once,
135 | "next": "/",
136 | ],
137 | headers: [
138 | "Referer": baseURL.appendingPathComponent("signin").absoluteString,
139 | ]
140 | )
141 | .responseString { response in
142 | if 200..<300 ~= response.response?.statusCode ?? 0 {
143 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
144 | if let title = doc?.at_css(".problem ul li")?.text {
145 | self.fetchData()
146 | let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
147 | alertController.addAction(UIAlertAction(title: "好", style: .default))
148 | self.present(alertController, animated: true)
149 | } else {
150 | DispatchQueue.global().async {
151 | let session = Session(usernameKey: "", passwordKey: "", captchaKey: "", once: "", username: session.username, password: session.password, captcha: nil)
152 | try? Keychain(service: "com.v2ex.www").accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .userPresence).set(session.toData(), key: "session")
153 | DispatchQueue.main.async {
154 | (((self.presentingViewController as? UITabBarController)?.viewControllers?.first as? UINavigationController)?.viewControllers.first as? TopicsController)?.signed()
155 | self.dismiss(animated: true)
156 | }
157 | }
158 | }
159 | } else {
160 | self.networkError()
161 | }
162 | self.hideHUD()
163 | }
164 | }
165 | }
166 |
167 | extension SignInController: UITableViewDataSource {
168 |
169 | func numberOfSections(in tableView: UITableView) -> Int {
170 | return 4
171 | }
172 |
173 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
174 | return [2, 1, 1, 1][section]
175 | }
176 |
177 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
178 | switch (indexPath.section, indexPath.row) {
179 | case (0, 0):
180 | let cell = tableView.dequeueReusableCell(withIdentifier: SignInUsernameCell.description(), for: indexPath) as? SignInUsernameCell ?? .init()
181 | cell.usernameField.text = session?.username
182 | return cell
183 |
184 | case (0, 1):
185 | let cell = tableView.dequeueReusableCell(withIdentifier: SignInPasswordCell.description(), for: indexPath) as? SignInPasswordCell ?? .init()
186 | cell.passwordField.text = session?.password
187 | return cell
188 |
189 | case (1, 0):
190 | let cell = tableView.dequeueReusableCell(withIdentifier: SignInCaptchaCell.description(), for: indexPath) as? SignInCaptchaCell ?? .init()
191 | cell.captchaField.text = session?.captcha
192 | if let once = session?.once {
193 | let url = URL(string: "\(baseURL.absoluteString)/_captcha?once=\(once)")
194 | cell.captchaImageView.setImage(withURL: url)
195 | }
196 | return cell
197 |
198 | case (2, 0):
199 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
200 | cell.accessoryView = termsSwitch
201 | cell.backgroundColor = .clear
202 | cell.textLabel?.text = "同意最终用户许可协议"
203 | cell.textLabel?.textColor = tableView.tintColor
204 | return cell
205 |
206 | case (3, 0):
207 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
208 | cell.selectionStyle = termsSwitch.isOn ? .default : .none
209 | cell.textLabel?.text = "登录"
210 | cell.textLabel?.textColor = termsSwitch.isOn ? tableView.tintColor : .tertiaryLabel
211 | return cell
212 |
213 | default:
214 | return .init()
215 | }
216 | }
217 | }
218 |
219 | extension SignInController: UITableViewDelegate {
220 |
221 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
222 | switch (indexPath.section, indexPath.row) {
223 | case (2, 0):
224 | let webViewController = WebViewController()
225 | webViewController.title = "最终用户许可协议"
226 | webViewController.url = baseURL.appendingPathComponent("about")
227 | navigationController?.pushViewController(webViewController, animated: true)
228 |
229 | case (3, 0):
230 | tableView.deselectRow(at: indexPath, animated: true)
231 | if termsSwitch.isOn { signIn() }
232 |
233 | default:
234 | break
235 | }
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/UserController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/16/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import Kanna
11 |
12 | class UserController: ViewController {
13 |
14 | private var activityIndicatorView: ActivityIndicatorView!
15 | private var isRefreshing = false { didSet { didSetRefreshing() } }
16 | private var networkErrorView: NetworkErrorView!
17 | private var noContentView: NoContentView!
18 | private var tableView: UITableView!
19 | public var user: User? { didSet { didSetUser() } }
20 |
21 | override init() {
22 | super.init()
23 |
24 | navigationItem.largeTitleDisplayMode = .never
25 | }
26 |
27 | override func loadView() {
28 | tableView = UITableView(frame: .zero, style: .grouped)
29 | tableView.cellLayoutMarginsFollowReadableWidth = true
30 | tableView.dataSource = self
31 | tableView.delegate = self
32 | tableView.refreshControl = UIRefreshControl()
33 | tableView.refreshControl?.addTarget(self, action: #selector(fetchData), for: .valueChanged)
34 | tableView.register(TopicsCell.self, forCellReuseIdentifier: TopicsCell.description())
35 | tableView.register(UserHeaderView.self, forHeaderFooterViewReuseIdentifier: UserHeaderView.description())
36 | view = tableView
37 |
38 | activityIndicatorView = ActivityIndicatorView()
39 | view.addSubview(activityIndicatorView)
40 |
41 | networkErrorView = NetworkErrorView()
42 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
43 | view.addSubview(networkErrorView)
44 |
45 | noContentView = NoContentView()
46 | noContentView.textLabel?.text = "无内容"
47 | view.addSubview(noContentView)
48 | }
49 |
50 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
51 | super.traitCollectionDidChange(previousTraitCollection)
52 |
53 | additionalSafeAreaInsets.top = -(navigationController?.navigationBar.intrinsicContentSize.height ?? 0)
54 |
55 | tryUpdateNavigationBarBackground()
56 | }
57 |
58 | override func viewWillAppear(_ animated: Bool) {
59 | super.viewWillAppear(animated)
60 |
61 | additionalSafeAreaInsets.top = -(navigationController?.navigationBar.intrinsicContentSize.height ?? 0)
62 |
63 | tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: animated) }
64 |
65 | tryUpdateNavigationBarBackground()
66 |
67 | if user?.topics == nil || !networkErrorView.isHidden { fetchData() }
68 | }
69 |
70 | override func viewDidAppear(_ animated: Bool) {
71 | super.viewDidAppear(animated)
72 |
73 | tryUpdateNavigationBarBackground()
74 | }
75 |
76 | override func viewWillDisappear(_ animated: Bool) {
77 | super.viewWillDisappear(animated)
78 |
79 | updateNavigationBarBackground()
80 | }
81 |
82 | @objc
83 | private func fetchData() {
84 | if isRefreshing { return }
85 | isRefreshing = true
86 | AF.request(
87 | baseURL
88 | .appendingPathComponent("member")
89 | .appendingPathComponent(user?.name ?? "")
90 | )
91 | .responseString { response in
92 | if 200..<300 ~= response.response?.statusCode ?? 0 {
93 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
94 | let json = [
95 | "id": Int("第 (\\d+) 号会员".r?.findFirst(in: doc?.at_css("#Main .box:nth-child(2) .gray")?.text ?? "")?.group(at: 1) ?? ""),
96 | "name": self.user?.name,
97 | "avatar": doc?.at_css(".avatar")?["src"],
98 | "isBlocked": doc?.at_css(".super.normal.button")?["value"] == "Unblock",
99 | "token": "t=(\\d+)".r?.findFirst(in: doc?.at_css(".super.normal.button")?["onclick"] ?? "")?.group(at: 1),
100 | "once": self.user?.once,
101 | "createdAt": "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\+\\d{2}:\\d{2}".r?.findFirst(in: doc?.at_css("#Main .box:nth-child(2) .gray")?.text ?? "")?.matched,
102 | "topics": doc?.css(".box .cell.item").map {
103 | [
104 | "id": Int("^/t/(\\d+)".r?.findFirst(in: $0.at_css(".item_title a")?["href"] ?? "")?.group(at: 1) ?? ""),
105 | "name": $0.at_css(".item_title a")?.text,
106 | "repliesCount": Int($0.at_css(".count_livid")?.text ?? ""),
107 | "repliedAt": "[^•]+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: $0.at_css(".topic_info, .small")?.text ?? "")?.matched.trimmingCharacters(in: .whitespaces),
108 | "node": [
109 | "name": $0.at_css(".node")?.text,
110 | "code": "[^/]+".r?.findFirst(in: $0.at_css(".node")?["href"] ?? "")?.matched,
111 | ],
112 | ] as [String: Any?]
113 | },
114 | ] as [String: Any?]
115 | self.user = try? User(json: json)
116 | self.noContentView.isHidden = self.user?.topics?.count ?? 0 > 0
117 | self.tableView.reloadData()
118 | } else {
119 | self.networkErrorView.isHidden = false
120 | }
121 | self.isRefreshing = false
122 | }
123 | }
124 |
125 | private func didSetRefreshing() {
126 | if isRefreshing {
127 | networkErrorView.isHidden = true
128 | noContentView.isHidden = true
129 | if tableView.refreshControl?.isRefreshing ?? false { return }
130 | activityIndicatorView.startAnimating()
131 | } else {
132 | tableView.refreshControl?.endRefreshing()
133 | activityIndicatorView.stopAnimating()
134 | }
135 | }
136 |
137 | private func didSetUser() {
138 | navigationItem.rightBarButtonItem = user?.once != nil ? UIBarButtonItem(title: "退出登录", style: .plain, target: self, action: #selector(signOut)) : UIBarButtonItem(title: user?.isBlocked == nil ? nil : user?.isBlocked ?? false ? "解除屏蔽" : "屏蔽", style: .plain, target: self, action: #selector(toggleBlock))
139 |
140 | userActivity?.webpageURL = baseURL
141 | .appendingPathComponent("member")
142 | .appendingPathComponent(user?.name ?? "")
143 | }
144 |
145 | @objc
146 | private func signOut(_ barButtonItem: UIBarButtonItem) {
147 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
148 | alertController.addAction(UIAlertAction(title: "退出登录", style: .destructive) { _ in
149 | self.showHUD()
150 | AF.request(
151 | self.baseURL.appendingPathComponent("signout"),
152 | parameters: [
153 | "once": self.user?.once ?? "",
154 | ],
155 | headers: [
156 | "Referer": self.baseURL.absoluteString,
157 | ]
158 | )
159 | .responseString { response in
160 | self.hideHUD()
161 | if 200..<300 ~= response.response?.statusCode ?? 0 {
162 | let navigationController = self.navigationController
163 | navigationController?.popViewController(animated: true)
164 | (navigationController?.viewControllers.first as? TopicsController)?.signed()
165 | } else {
166 | self.networkError()
167 | }
168 | }
169 | })
170 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
171 | alertController.popoverPresentationController?.barButtonItem = barButtonItem
172 | present(alertController, animated: true)
173 | }
174 |
175 | @objc
176 | private func toggleBlock(_ barButtonItem: UIBarButtonItem) {
177 | if ((tabBarController?.viewControllers?.first as? UINavigationController)?.viewControllers.first as? TopicsController)?.user?.once == nil { return signInRequired(barButtonItem) }
178 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
179 | alertController.addAction(UIAlertAction(title: user?.isBlocked ?? false ? "解除屏蔽" : "屏蔽", style: .destructive) { _ in
180 | self.showHUD()
181 | AF.request(
182 | self.baseURL
183 | .appendingPathComponent(self.user?.isBlocked ?? false ? "unblock" : "block")
184 | .appendingPathComponent(String(self.user?.id ?? 0)),
185 | parameters: [
186 | "t": self.user?.token ?? "",
187 | ]
188 | )
189 | .responseString { response in
190 | if 200..<300 ~= response.response?.statusCode ?? 0 {
191 | self.navigationController?.popViewController(animated: true)
192 | } else {
193 | self.networkError()
194 | }
195 | self.hideHUD()
196 | }
197 | })
198 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
199 | alertController.popoverPresentationController?.barButtonItem = barButtonItem
200 | present(alertController, animated: true)
201 | }
202 |
203 | private func updateNavigationBarBackground() {
204 | guard let navigationBar = navigationController?.navigationBar else { return }
205 | let headerView = tableView.headerView(forSection: 0)
206 | let alpha = self != navigationController?.topViewController || headerView == nil ? 1 : (tableView.contentOffset.y + view.safeAreaInsets.top) / ((headerView?.frame.height ?? 0) - navigationBar.frame.height)
207 | navigationBar.subviews.first { $0.classForCoder.description() == "_UIBarBackground" }?.alpha = max(0, min(1, alpha))
208 | navigationItem.title = alpha < 1 ? nil : user?.name
209 | }
210 |
211 | private func tryUpdateNavigationBarBackground() {
212 | for interval in [0.001, 0.01, 0.1] {
213 | DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
214 | self.updateNavigationBarBackground()
215 | }
216 | }
217 | }
218 | }
219 |
220 | extension UserController: UIScrollViewDelegate {
221 |
222 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
223 | if self != navigationController?.topViewController { return }
224 | updateNavigationBarBackground()
225 | }
226 | }
227 |
228 | extension UserController: UITableViewDataSource {
229 |
230 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
231 | return user?.topics?.count ?? 0
232 | }
233 |
234 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
235 | let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: UserHeaderView.description()) as? UserHeaderView
236 | view?.user = user
237 | return view
238 | }
239 |
240 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
241 | let cell = tableView.dequeueReusableCell(withIdentifier: TopicsCell.description(), for: indexPath) as? TopicsCell ?? .init()
242 | cell.tableViewStyle = tableView.style
243 | cell.topic = user?.topics?[indexPath.row]
244 | return cell
245 | }
246 | }
247 |
248 | extension UserController: UITableViewDelegate {
249 |
250 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
251 | let topicController = TopicController()
252 | topicController.topic = user?.topics?[indexPath.row]
253 | topicController.topic?.user = user
254 | navigationController?.pushViewController(topicController, animated: true)
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/TopicsController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicsController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/7/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import Kanna
11 | import KeychainAccess
12 |
13 | class TopicsController: ViewController {
14 |
15 | private var activityIndicatorView: ActivityIndicatorView!
16 | private var isRefreshing = false { didSet { didSetRefreshing() } }
17 | private var networkErrorView: NetworkErrorView!
18 | public var node = Node.all { didSet { didSetNode() } }
19 | private var nodesView: TopicsNodesView?
20 | private var noContentView: NoContentView!
21 | private var tableView: UITableView!
22 | private var topics: [Topic] = []
23 | private var topicsIsLoaded = false
24 | private var topicsNextPage = 1
25 | public var user: User? { didSet { didSetUser() } }
26 |
27 | override func loadView() {
28 | tableView = UITableView()
29 | tableView.cellLayoutMarginsFollowReadableWidth = true
30 | tableView.dataSource = self
31 | tableView.delegate = self
32 | tableView.refreshControl = UIRefreshControl()
33 | tableView.refreshControl?.addTarget(self, action: #selector(fetchData), for: .valueChanged)
34 | tableView.register(TopicsCell.self, forCellReuseIdentifier: TopicsCell.description())
35 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.description())
36 | tableView.tableFooterView = UIView()
37 | tableView.verticalScrollIndicatorInsets.top = self == navigationController?.viewControllers.first ? 44 : 0
38 | view = tableView
39 |
40 | activityIndicatorView = ActivityIndicatorView()
41 | view.addSubview(activityIndicatorView)
42 |
43 | networkErrorView = NetworkErrorView()
44 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
45 | view.addSubview(networkErrorView)
46 |
47 | noContentView = NoContentView()
48 | noContentView.textLabel?.text = "无内容"
49 | view.addSubview(noContentView)
50 |
51 | nodesView = self == navigationController?.viewControllers.first ? TopicsNodesView() : nil
52 | tableView.tableHeaderView = nodesView
53 | nodesView?.snp.makeConstraints { make in
54 | make.width.equalToSuperview()
55 | make.height.equalTo(44)
56 | }
57 | }
58 |
59 | override func viewSafeAreaInsetsDidChange() {
60 | super.viewSafeAreaInsetsDidChange()
61 |
62 | nodesView?.contentInset.left = view.layoutMargins.left - view.safeAreaInsets.left
63 | nodesView?.contentInset.right = view.layoutMargins.right - view.safeAreaInsets.right
64 | }
65 |
66 | override func viewLayoutMarginsDidChange() {
67 | super.viewLayoutMarginsDidChange()
68 |
69 | nodesView?.contentInset.left = view.layoutMargins.left - view.safeAreaInsets.left
70 | nodesView?.contentInset.right = view.layoutMargins.right - view.safeAreaInsets.right
71 | }
72 |
73 | override func viewWillAppear(_ animated: Bool) {
74 | super.viewWillAppear(animated)
75 |
76 | title = self == navigationController?.viewControllers.first ? Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String : node.name
77 |
78 | tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: animated) }
79 |
80 | if topics.count == 0 && !topicsIsLoaded || !networkErrorView.isHidden { fetchData() }
81 | }
82 |
83 | @objc
84 | func fetchData() {
85 | if activityIndicatorView.isAnimating { tableView.refreshControl?.endRefreshing() }
86 | if isRefreshing { return }
87 | isRefreshing = true
88 | AF.request(
89 | self != navigationController?.viewControllers.first ? baseURL.appendingPathComponent("go").appendingPathComponent(node.code ?? "") : node.code == Node.all.code && topics.count > 0 && !(tableView.refreshControl?.isRefreshing ?? false) ? baseURL.appendingPathComponent("recent") : baseURL,
90 | parameters: [
91 | "p": tableView.refreshControl?.isRefreshing ?? false ? 1 : topicsNextPage,
92 | "tab": node.code ?? "",
93 | ]
94 | )
95 | .responseString { response in
96 | if self.tableView.refreshControl?.isRefreshing ?? false {
97 | self.topics = []
98 | self.topicsIsLoaded = false
99 | }
100 | if 200..<300 ~= response.response?.statusCode ?? 0 {
101 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
102 | if self == self.navigationController?.viewControllers.first {
103 | let userJSON = [
104 | "name": doc?.at_css("#Top .tools a:nth-child(2)")?.text,
105 | "avatar": doc?.at_css("#Rightbar .box:nth-child(2) .avatar")?["src"],
106 | "once": "\\d{5}".r?.findFirst(in: doc?.at_css("#Top .tools a:last-child")?["onclick"] ?? "")?.matched,
107 | ]
108 | self.user = try? User(json: userJSON)
109 | }
110 | if self.nodesView != nil && self.nodesView?.nodes == nil {
111 | let nodesJSON = doc?.css("#Tabs a").map {
112 | [
113 | "name": $0.text,
114 | "code": "[^=]+$".r?.findFirst(in: $0["href"] ?? "")?.matched,
115 | ]
116 | }
117 | self.nodesView?.nodes = (try? [Node](json: nodesJSON ?? []))?.filter { $0.code != Node.all.code }
118 | }
119 | self.topicsIsLoaded = doc?.at_css(".normal_page_right:not(.disable_now)") == nil && (self.node.code != Node.all.code || self.topics.count > 0 && !(self.tableView.refreshControl?.isRefreshing ?? false) || self.user?.once == nil)
120 | self.topicsNextPage = Int(doc?.at_css(".page_current + a")?.text ?? "") ?? 1
121 | let json = doc?.css("#Main .cell.item, #TopicsNode .cell").map {
122 | [
123 | "id": Int("^/t/(\\d+)".r?.findFirst(in: $0.at_css(".item_title a")?["href"] ?? "")?.group(at: 1) ?? ""),
124 | "name": $0.at_css(".item_title a")?.text,
125 | "repliesCount": Int($0.at_css(".count_livid")?.text ?? ""),
126 | "repliedAt": "[^•]+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: ($0.at_css(".topic_info") ?? $0.at_css(".small"))?.text ?? "")?.matched.trimmingCharacters(in: .whitespaces),
127 | "isSticky": "corner_star".r?.matches($0["style"] ?? ""),
128 | "user": [
129 | "name": $0.at_css("strong")?.text,
130 | "avatar": $0.at_css("img")?["src"],
131 | ],
132 | "node": [
133 | "name": $0.at_css(".node")?.text,
134 | "code": "[^/]+".r?.findFirst(in: $0.at_css(".node")?["href"] ?? "")?.matched,
135 | ],
136 | ] as [String: Any?]
137 | }
138 | self.topics += (try? [Topic](json: json ?? [])) ?? []
139 | self.noContentView.isHidden = self.topics.count > 0
140 | } else {
141 | self.networkErrorView.isHidden = false
142 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
143 | self.fetchData()
144 | }
145 | }
146 | self.tableView.reloadData()
147 | self.isRefreshing = false
148 | }
149 | }
150 |
151 | private func didSetRefreshing() {
152 | if isRefreshing {
153 | networkErrorView.isHidden = true
154 | noContentView.isHidden = true
155 | nodesView?.isEnabled = false
156 | if tableView.refreshControl?.isRefreshing ?? false { return }
157 | activityIndicatorView.startAnimating()
158 | } else {
159 | nodesView?.isEnabled = true
160 | tableView.refreshControl?.endRefreshing()
161 | activityIndicatorView.stopAnimating()
162 | }
163 | }
164 |
165 | private func didSetNode() {
166 | userActivity?.webpageURL = self == navigationController?.viewControllers.first ? baseURL : baseURL.appendingPathComponent("go").appendingPathComponent(node.code ?? "")
167 |
168 | if isViewLoaded { refetchData() }
169 | }
170 |
171 | private func didSetUser() {
172 | if user == nil {
173 | navigationItem.rightBarButtonItem = nil
174 | } else if user?.once == nil {
175 | navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "person.crop.circle"), style: .plain, target: self, action: #selector(showSignIn))
176 | } else {
177 | let imageView = UIImageView()
178 | imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showUser)))
179 | imageView.backgroundColor = .secondarySystemBackground
180 | imageView.clipsToBounds = true
181 | imageView.isUserInteractionEnabled = true
182 | imageView.layer.cornerRadius = 14
183 | imageView.setImage(withURL: user?.avatar)
184 | imageView.snp.makeConstraints { $0.size.equalTo(28) }
185 | navigationItem.rightBarButtonItem = UIBarButtonItem(customView: imageView)
186 | }
187 | }
188 |
189 | private func refetchData() {
190 | topics = []
191 | topicsIsLoaded = false
192 | topicsNextPage = 1
193 | tableView.reloadData()
194 | fetchData()
195 | }
196 |
197 | internal func signed() {
198 | user = nil
199 | refetchData()
200 | }
201 |
202 | @objc
203 | private func showUser() {
204 | let userController = UserController()
205 | userController.user = user
206 | navigationController?.pushViewController(userController, animated: true)
207 | }
208 |
209 | @objc
210 | internal func showSignIn(_ sender: Any? = nil) {
211 | DispatchQueue.global().async {
212 | let data = (try? Keychain(service: "com.v2ex.www").getData("session")) ?? .init()
213 | DispatchQueue.main.async {
214 | let signInController = SignInController()
215 | signInController.session = try? Session(data: data)
216 | let navigationController = UINavigationController(rootViewController: signInController)
217 | navigationController.modalPresentationStyle = sender is UIBarButtonItem ? .popover : .formSheet
218 | navigationController.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem
219 | self.present(navigationController, animated: true)
220 | }
221 | }
222 | }
223 | }
224 |
225 | extension TopicsController: UITableViewDataSource {
226 |
227 | func numberOfSections(in tableView: UITableView) -> Int {
228 | return 2
229 | }
230 |
231 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
232 | return [
233 | topics.count,
234 | node.code == Node.all.code && user?.once == nil && topics.count > 0 ? 1 : 0,
235 | ][section]
236 | }
237 |
238 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
239 | switch indexPath.section {
240 | case 0:
241 | let cell = tableView.dequeueReusableCell(withIdentifier: TopicsCell.description(), for: indexPath) as? TopicsCell ?? .init()
242 | cell.tableViewStyle = tableView.style
243 | cell.topic = topics[indexPath.row]
244 | return cell
245 |
246 | case 1:
247 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.description(), for: indexPath)
248 | cell.textLabel?.text = "登录后查看更多"
249 | cell.textLabel?.textAlignment = .center
250 | cell.textLabel?.textColor = tableView.tintColor
251 | return cell
252 |
253 | default:
254 | return .init()
255 | }
256 | }
257 | }
258 |
259 | extension TopicsController: UITableViewDelegate {
260 |
261 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
262 | if !topicsIsLoaded && indexPath.row == topics.count - 1 { fetchData() }
263 | }
264 |
265 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
266 | switch indexPath.section {
267 | case 0:
268 | let topicController = TopicController()
269 | topicController.topic = topics[indexPath.row]
270 | navigationController?.pushViewController(topicController, animated: true)
271 |
272 | case 1:
273 | tableView.deselectRow(at: indexPath, animated: true)
274 | showSignIn()
275 |
276 | default:
277 | break
278 | }
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/V2EX/Classes/Controllers/TopicController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicController.swift
3 | // V2EX
4 | //
5 | // Created by Jianqiu Xiao on 4/8/19.
6 | // Copyright © 2019 Jianqiu Xiao. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import Kanna
11 | import TUSafariActivity
12 |
13 | class TopicController: ViewController {
14 |
15 | private var activityIndicatorView: ActivityIndicatorView!
16 | private var bodyCell: TopicBodyCell?
17 | private var commentCells: [TopicCommentCell] = []
18 | private var isRefreshing = false { didSet { didSetRefreshing() } }
19 | private var networkErrorView: NetworkErrorView!
20 | private var replies: [Reply]?
21 | private var replyCells: [TopicReplyCell] = []
22 | private var tableView: UITableView!
23 | public var topic: Topic? { didSet { didSetTopic() } }
24 |
25 | override init() {
26 | super.init()
27 |
28 | hidesBottomBarWhenPushed = true
29 |
30 | navigationItem.largeTitleDisplayMode = .never
31 |
32 | toolbarItems = [
33 | UIBarButtonItem(image: nil, style: .plain, target: self, action: #selector(toggleFavorite)),
34 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
35 | UIBarButtonItem(barButtonSystemItem: .reply, target: self, action: #selector(showCompose)),
36 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
37 | UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(ignore)),
38 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
39 | UIBarButtonItem(image: UIImage(systemName: "flag"), style: .plain, target: self, action: #selector(showReport)),
40 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
41 | UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(action)),
42 | ]
43 | }
44 |
45 | override func loadView() {
46 | tableView = UITableView()
47 | tableView.cellLayoutMarginsFollowReadableWidth = true
48 | tableView.dataSource = self
49 | tableView.delegate = self
50 | tableView.refreshControl = UIRefreshControl()
51 | tableView.refreshControl?.addTarget(self, action: #selector(fetchData), for: .valueChanged)
52 | tableView.register(TopicNameCell.self, forCellReuseIdentifier: TopicNameCell.description())
53 | tableView.tableFooterView = UIView()
54 | view = tableView
55 |
56 | activityIndicatorView = ActivityIndicatorView()
57 | view.addSubview(activityIndicatorView)
58 |
59 | networkErrorView = NetworkErrorView()
60 | networkErrorView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fetchData)))
61 | view.addSubview(networkErrorView)
62 | }
63 |
64 | override func viewWillAppear(_ animated: Bool) {
65 | super.viewWillAppear(animated)
66 |
67 | navigationController?.isToolbarHidden = false
68 |
69 | if topic?.clicksCount == nil || !networkErrorView.isHidden { fetchData() }
70 | }
71 |
72 | override func viewWillDisappear(_ animated: Bool) {
73 | super.viewWillDisappear(animated)
74 |
75 | navigationController?.isToolbarHidden = true
76 | }
77 |
78 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
79 | super.viewWillTransition(to: size, with: coordinator)
80 |
81 | coordinator.animate(alongsideTransition: nil) { _ in
82 | self.didSetTopic()
83 | self.replyCells = (self.replies ?? []).map { _ in TopicReplyCell() }
84 | self.tableView.reloadData()
85 | }
86 | }
87 |
88 | @objc
89 | private func fetchData() {
90 | if isRefreshing { return }
91 | isRefreshing = true
92 | AF.request(
93 | baseURL
94 | .appendingPathComponent("t")
95 | .appendingPathComponent(String(topic?.id ?? 0)),
96 | parameters: [
97 | "p": 1,
98 | ]
99 | )
100 | .responseString { response in
101 | if 200..<300 ~= response.response?.statusCode ?? 0 {
102 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
103 | let name = doc?.at_css("h1")?.text
104 | let bodyHTML = doc?.at_css(".topic_content")?.innerHTML
105 | let clicksCount = Int("(\\d+) 次点击".r?.findFirst(in: doc?.at_css(".header .gray")?.text ?? "")?.group(at: 1) ?? "")
106 | let favoritesCount = Int("(\\d+) 人收藏".r?.findFirst(in: doc?.at_css(".topic_stats")?.text ?? "")?.group(at: 1) ?? "")
107 | let repliesCount = Int("^\\d+".r?.findFirst(in: doc?.at_css("#Main .box:nth-child(4) .gray")?.text ?? "")?.matched ?? "")
108 | let createdAt = "[^·]+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: doc?.at_css(".header .gray")?.text ?? "")?.matched.trimmingCharacters(in: .whitespacesAndNewlines)
109 | let isFavorite = ["加入收藏": false, "取消收藏": true][doc?.at_css(".topic_buttons .tb")?.text]
110 | let favoriteToken = "[^=]+$".r?.findFirst(in: doc?.at_css(".topic_buttons .tb")?["href"] ?? "")?.matched
111 | let once = Int(doc?.at_css("input[name=once]")?["value"] ?? "")
112 | let repliesNextPage = Int(doc?.at_css(".page_current + a")?.text ?? "")
113 | let userJSON = [
114 | "name": doc?.at_css(".header .gray a")?.text,
115 | "avatar": doc?.at_css(".header .avatar")?["src"],
116 | ]
117 | let nodeJSON = [
118 | "name": doc?.css(".header a")[2].text,
119 | "code": "[^/]+$".r?.findFirst(in: doc?.css(".header a")[2]["href"] ?? "")?.matched,
120 | ]
121 | let commentsJSON = doc?.css(".subtle").map {
122 | [
123 | "bodyHTML": $0.at_css(".topic_content")?.innerHTML,
124 | "createdAt": "[^·]+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: $0.at_css(".fade")?.text ?? "")?.matched.trimmingCharacters(in: .whitespacesAndNewlines),
125 | ] as [String: Any?]
126 | }
127 | let json = [
128 | "id": self.topic?.id,
129 | "name": name,
130 | "bodyHTML": bodyHTML,
131 | "clicksCount": clicksCount,
132 | "favoritesCount": favoritesCount,
133 | "repliesCount": repliesCount,
134 | "createdAt": createdAt,
135 | "isFavorite": isFavorite,
136 | "favoriteToken": favoriteToken,
137 | "once": once,
138 | "repliesNextPage": repliesNextPage,
139 | "user": userJSON,
140 | "node": nodeJSON,
141 | "comments": commentsJSON,
142 | ] as [String: Any?]
143 | self.topic = try? Topic(json: json)
144 | let repliesJSON = doc?.css("#Main .box:nth-child(4) .cell[id]").map {
145 | [
146 | "bodyHTML": $0.at_css(".reply_content")?.innerHTML,
147 | "createdAt": ".+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: $0.at_css(".ago")?.text ?? "")?.matched,
148 | "user": [
149 | "name": $0.at_css(".dark")?.text,
150 | "avatar": $0.at_css(".avatar")?["src"],
151 | ],
152 | ] as [String: Any?]
153 | }
154 | self.replies = (try? [Reply](json: repliesJSON ?? [])) ?? []
155 | self.replyCells = (self.replies ?? []).map { _ in TopicReplyCell() }
156 | self.tableView.reloadData()
157 | } else {
158 | self.networkErrorView.isHidden = false
159 | }
160 | self.isRefreshing = false
161 | }
162 | }
163 |
164 | private func fetchReplies() {
165 | guard let page = topic?.repliesNextPage else { return }
166 | if isRefreshing { return }
167 | isRefreshing = true
168 | AF.request(
169 | baseURL
170 | .appendingPathComponent("t")
171 | .appendingPathComponent(String(topic?.id ?? 0)),
172 | parameters: [
173 | "p": page,
174 | ]
175 | )
176 | .responseString { response in
177 | if 200..<300 ~= response.response?.statusCode ?? 0 {
178 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
179 | self.topic?.repliesNextPage = Int(doc?.at_css(".page_current + a")?.text ?? "")
180 | let json = doc?.css("#Main .box:nth-child(4) .cell[id]").map {
181 | [
182 | "bodyHTML": $0.at_css(".reply_content")?.innerHTML,
183 | "createdAt": ".+前|刚刚|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}".r?.findFirst(in: $0.at_css(".ago")?.text ?? "")?.matched,
184 | "user": [
185 | "name": $0.at_css(".dark")?.text,
186 | "avatar": $0.at_css(".avatar")?["src"],
187 | ],
188 | ] as [String: Any?]
189 | }
190 | let replies = (try? [Reply](json: json ?? [])) ?? []
191 | self.replies = (self.replies ?? []) + replies
192 | self.replyCells += replies.map { _ in TopicReplyCell() }
193 | self.tableView.reloadSections(IndexSet([3]), with: .none)
194 | } else {
195 | self.networkErrorView.isHidden = false
196 | }
197 | self.isRefreshing = false
198 | }
199 | }
200 |
201 | private func didSetRefreshing() {
202 | if isRefreshing {
203 | networkErrorView.isHidden = true
204 | if tableView.refreshControl?.isRefreshing ?? false { return }
205 | activityIndicatorView.startAnimating()
206 | } else {
207 | tableView.refreshControl?.endRefreshing()
208 | activityIndicatorView.stopAnimating()
209 | }
210 | }
211 |
212 | private func didSetTopic() {
213 | bodyCell = TopicBodyCell()
214 | commentCells = (topic?.comments ?? []).map { _ in TopicCommentCell() }
215 |
216 | toolbarItems?.first?.image = topic?.isFavorite ?? false ? UIImage(systemName: "star.fill") : UIImage(systemName: "star")
217 |
218 | userActivity?.webpageURL = baseURL
219 | .appendingPathComponent("t")
220 | .appendingPathComponent(String(topic?.id ?? 0))
221 | }
222 |
223 | @objc
224 | func action(_ barButtonItem: UIBarButtonItem) {
225 | let url = baseURL
226 | .appendingPathComponent("t")
227 | .appendingPathComponent(String(topic?.id ?? 0))
228 | let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: [TUSafariActivity()])
229 | activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
230 | present(activityViewController, animated: true)
231 | }
232 |
233 | @objc
234 | private func toggleFavorite(_ sender: Any) {
235 | if ((tabBarController?.viewControllers?.first as? UINavigationController)?.viewControllers.first as? TopicsController)?.user?.once == nil { return signInRequired(sender) }
236 | showHUD()
237 | AF.request(
238 | baseURL
239 | .appendingPathComponent(topic?.isFavorite ?? false ? "unfavorite" : "favorite")
240 | .appendingPathComponent("topic")
241 | .appendingPathComponent(String(topic?.id ?? 0)),
242 | parameters: [
243 | "t": topic?.favoriteToken ?? "",
244 | ],
245 | headers: [
246 | "Referer": baseURL.appendingPathComponent("t").appendingPathComponent(String(topic?.id ?? 0)).absoluteString,
247 | ]
248 | )
249 | .responseString { response in
250 | if 200..<300 ~= response.response?.statusCode ?? 0 {
251 | let doc = try? HTML(html: response.value ?? "", encoding: .utf8)
252 | self.topic?.favoriteToken = "[^=]+$".r?.findFirst(in: doc?.at_css(".topic_buttons .tb")?["href"] ?? "")?.matched
253 | self.topic?.isFavorite?.toggle()
254 | } else {
255 | self.networkError()
256 | }
257 | self.hideHUD()
258 | }
259 | }
260 |
261 | @objc
262 | private func showCompose(_ sender: Any) {
263 | showCompose(sender, nil)
264 | }
265 |
266 | internal func showCompose(_ sender: Any, _ reply: Reply? = nil) {
267 | if ((tabBarController?.viewControllers?.first as? UINavigationController)?.viewControllers.first as? TopicsController)?.user?.once == nil { return signInRequired(sender) }
268 | let composeController = ComposeController()
269 | composeController.reply = reply
270 | composeController.topic = topic
271 | let navigationController = UINavigationController(rootViewController: composeController)
272 | navigationController.modalPresentationStyle = sender is UIBarButtonItem ? .popover : .formSheet
273 | navigationController.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem
274 | present(navigationController, animated: true)
275 | }
276 |
277 | @objc
278 | func ignore(_ barButtonItem: UIBarButtonItem) {
279 | if ((tabBarController?.viewControllers?.first as? UINavigationController)?.viewControllers.first as? TopicsController)?.user?.once == nil { return signInRequired(barButtonItem) }
280 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
281 | alertController.addAction(UIAlertAction(title: "忽略", style: .destructive) { _ in
282 | self.showHUD()
283 | AF.request(
284 | self.baseURL.appendingPathComponent("/ignore/topic/\(self.topic?.id ?? 0)"),
285 | parameters: [
286 | "once": self.topic?.once ?? "",
287 | ],
288 | headers: [
289 | "Referer": self.baseURL.appendingPathComponent("t").appendingPathComponent(String(self.topic?.id ?? 0)).absoluteString,
290 | ]
291 | )
292 | .response { response in
293 | if 200..<300 ~= response.response?.statusCode ?? 0 {
294 | self.navigationController?.popViewController(animated: true)
295 | } else {
296 | self.networkError()
297 | }
298 | self.hideHUD()
299 | }
300 | })
301 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
302 | alertController.popoverPresentationController?.barButtonItem = barButtonItem
303 | present(alertController, animated: true)
304 | }
305 |
306 | @objc
307 | private func showReport(_ barButtonItem: UIBarButtonItem) {
308 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
309 | alertController.addAction(UIAlertAction(title: "举报", style: .destructive) { _ in
310 | self.report()
311 | })
312 | alertController.addAction(UIAlertAction(title: "取消", style: .cancel))
313 | alertController.popoverPresentationController?.barButtonItem = barButtonItem
314 | present(alertController, animated: true)
315 | }
316 |
317 | internal func report() {
318 | showHUD()
319 | AF.request(
320 | URL(string: "https://ruby-china.net/v2ex") ?? .init(fileURLWithPath: "")
321 | ).response { response in
322 | if 200..<300 ~= response.response?.statusCode ?? 0 {
323 | let alertController = UIAlertController(title: "已举报", message: "24小时之内将会处理", preferredStyle: .alert)
324 | alertController.addAction(UIAlertAction(title: "好", style: .default))
325 | self.present(alertController, animated: true)
326 | } else {
327 | self.networkError()
328 | }
329 | self.hideHUD()
330 | }
331 | }
332 | }
333 |
334 | extension TopicController: UITableViewDataSource {
335 |
336 | func numberOfSections(in tableView: UITableView) -> Int {
337 | return 4
338 | }
339 |
340 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
341 | if section == 3 && replies != nil { return "\(topic?.repliesCount ?? 0) 回复" }
342 | return nil
343 | }
344 |
345 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
346 | return [
347 | 1,
348 | topic?.bodyHTML != nil ? 1 : 0,
349 | topic?.comments?.count ?? 0,
350 | replies?.count ?? 0,
351 | ][section]
352 | }
353 |
354 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
355 | switch indexPath.section {
356 | case 0:
357 | let cell = tableView.dequeueReusableCell(withIdentifier: TopicNameCell.description(), for: indexPath) as? TopicNameCell ?? .init()
358 | cell.topic = topic
359 | return cell
360 |
361 | case 1:
362 | let cell = bodyCell ?? .init()
363 | cell.topic = topic
364 | cell.layoutIfNeeded()
365 | return cell
366 |
367 | case 2:
368 | let cell = commentCells[indexPath.row]
369 | cell.comment = topic?.comments?[indexPath.row]
370 | cell.comment?.index = indexPath.row
371 | cell.layoutIfNeeded()
372 | return cell
373 |
374 | case 3:
375 | let cell = replyCells[indexPath.row]
376 | cell.reply = replies?[indexPath.row]
377 | cell.reply?.index = indexPath.row
378 | cell.layoutIfNeeded()
379 | return cell
380 |
381 | default:
382 | return .init()
383 | }
384 | }
385 | }
386 |
387 | extension TopicController: UITableViewDelegate {
388 |
389 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
390 | if indexPath.section == 3 && indexPath.row == (replies?.count ?? 0) - 1 { fetchReplies() }
391 | }
392 |
393 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
394 | switch indexPath.section {
395 | case 3:
396 | return UISwipeActionsConfiguration(actions: [
397 | UIContextualAction(style: .normal, title: "回复") { _, _, completionHandler in
398 | completionHandler(true)
399 | let cell = tableView.cellForRow(at: indexPath) ?? .init()
400 | self.showCompose(cell, self.replies?[indexPath.row])
401 | },
402 | ])
403 |
404 | default:
405 | return UISwipeActionsConfiguration(actions: [])
406 | }
407 | }
408 | }
409 |
--------------------------------------------------------------------------------
/V2EX.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 51;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 95B6016E29031068964A3358 /* Pods_V2EX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 507B5C8E95694E8A84014586 /* Pods_V2EX.framework */; };
11 | B227C89C22A6B55400C19F41 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B227C89B22A6B55400C19F41 /* UIImageView.swift */; };
12 | B23B1E3F22B7AEED005C7931 /* MoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B23B1E3E22B7AEED005C7931 /* MoreController.swift */; };
13 | B24B36A7225A27210004BCA0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36A6225A27210004BCA0 /* AppDelegate.swift */; };
14 | B24B36AE225A27220004BCA0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B24B36AD225A27220004BCA0 /* Assets.xcassets */; };
15 | B24B36B1225A27220004BCA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B24B36AF225A27220004BCA0 /* LaunchScreen.storyboard */; };
16 | B24B36BB225A2A070004BCA0 /* TopicsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36BA225A2A070004BCA0 /* TopicsController.swift */; };
17 | B24B36BE225B3DF20004BCA0 /* Topic.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36BD225B3DF20004BCA0 /* Topic.swift */; };
18 | B24B36C0225B60EF0004BCA0 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36BF225B60EF0004BCA0 /* Node.swift */; };
19 | B24B36C2225B61190004BCA0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36C1225B61190004BCA0 /* User.swift */; };
20 | B24B36C6225B65A60004BCA0 /* TopicsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36C5225B65A60004BCA0 /* TopicsCell.swift */; };
21 | B24B36C8225B98810004BCA0 /* TopicController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36C7225B98810004BCA0 /* TopicController.swift */; };
22 | B24B36CA225B9E070004BCA0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36C9225B9E070004BCA0 /* ViewController.swift */; };
23 | B24B36CD225C4D770004BCA0 /* TopicNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24B36CC225C4D770004BCA0 /* TopicNameCell.swift */; };
24 | B2536D5022667BC6002E2509 /* SignInCaptchaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2536D4F22667BC6002E2509 /* SignInCaptchaCell.swift */; };
25 | B2536D522266D377002E2509 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2536D512266D377002E2509 /* Session.swift */; };
26 | B27F7AEC22A7E040003D52C9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B27F7AEB22A7E040003D52C9 /* GoogleService-Info.plist */; };
27 | B2971DB6226972DF000C6645 /* TopicCommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2971DB5226972DF000C6645 /* TopicCommentCell.swift */; };
28 | B2971DB82269BF75000C6645 /* NodesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2971DB72269BF75000C6645 /* NodesController.swift */; };
29 | B2971DBA2269BFB5000C6645 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2971DB92269BFB5000C6645 /* Section.swift */; };
30 | B29F7AAA2265A8FE00A6E7DE /* UserController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B29F7AA92265A8FE00A6E7DE /* UserController.swift */; };
31 | B29F7AAD22662D3C00A6E7DE /* UserHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B29F7AAC22662D3C00A6E7DE /* UserHeaderView.swift */; };
32 | B29F7AAF226643CA00A6E7DE /* SignInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B29F7AAE226643CA00A6E7DE /* SignInController.swift */; };
33 | B2B2FEE3225E337D00E5364B /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEE2225E337D00E5364B /* Reply.swift */; };
34 | B2B2FEE5225E33F000E5364B /* TopicBodyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEE4225E33F000E5364B /* TopicBodyCell.swift */; };
35 | B2B2FEE7225E34DD00E5364B /* TopicWebCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEE6225E34DD00E5364B /* TopicWebCell.swift */; };
36 | B2B2FEE9225E353F00E5364B /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEE8225E353F00E5364B /* UIView.swift */; };
37 | B2B2FEEB225E3D8A00E5364B /* TopicReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEEA225E3D8A00E5364B /* TopicReplyCell.swift */; };
38 | B2B2FEED225F53BC00E5364B /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEEC225F53BC00E5364B /* ActivityIndicatorView.swift */; };
39 | B2B2FEEF225F53D500E5364B /* NetworkErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEEE225F53D500E5364B /* NetworkErrorView.swift */; };
40 | B2B2FEF1225F53EB00E5364B /* NoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEF0225F53EB00E5364B /* NoContentView.swift */; };
41 | B2B2FEF3225F622500E5364B /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B2FEF2225F622500E5364B /* WebViewController.swift */; };
42 | B2CA8810226D848A00F8B12F /* TopicsNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CA880F226D848A00F8B12F /* TopicsNodeButton.swift */; };
43 | B2CA8812226D97E900F8B12F /* TopicsNodesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CA8811226D97E900F8B12F /* TopicsNodesView.swift */; };
44 | B2CA8814226DA79500F8B12F /* SignInUsernameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CA8813226DA79500F8B12F /* SignInUsernameCell.swift */; };
45 | B2CA8816226DA97300F8B12F /* SignInPasswordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CA8815226DA97300F8B12F /* SignInPasswordCell.swift */; };
46 | B2E3704A22AB70980072DDCC /* ComposeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E3704922AB70980072DDCC /* ComposeController.swift */; };
47 | B2E85F8F226717F100D20A3F /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E85F8E226717F100D20A3F /* Comment.swift */; };
48 | /* End PBXBuildFile section */
49 |
50 | /* Begin PBXFileReference section */
51 | 2E0AC9774B17E9B73450E858 /* Pods-V2EX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-V2EX.debug.xcconfig"; path = "Target Support Files/Pods-V2EX/Pods-V2EX.debug.xcconfig"; sourceTree = ""; };
52 | 507B5C8E95694E8A84014586 /* Pods_V2EX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_V2EX.framework; sourceTree = BUILT_PRODUCTS_DIR; };
53 | 76CB0DEDDE91A97AC1E5014E /* Pods-V2EX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-V2EX.release.xcconfig"; path = "Target Support Files/Pods-V2EX/Pods-V2EX.release.xcconfig"; sourceTree = ""; };
54 | B227C89B22A6B55400C19F41 /* UIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageView.swift; sourceTree = ""; };
55 | B23B1E3E22B7AEED005C7931 /* MoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreController.swift; sourceTree = ""; };
56 | B24B36A3225A27210004BCA0 /* V2EX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = V2EX.app; sourceTree = BUILT_PRODUCTS_DIR; };
57 | B24B36A6225A27210004BCA0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
58 | B24B36AD225A27220004BCA0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
59 | B24B36B2225A27220004BCA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
60 | B24B36BA225A2A070004BCA0 /* TopicsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsController.swift; sourceTree = ""; };
61 | B24B36BD225B3DF20004BCA0 /* Topic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Topic.swift; sourceTree = ""; };
62 | B24B36BF225B60EF0004BCA0 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; };
63 | B24B36C1225B61190004BCA0 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
64 | B24B36C5225B65A60004BCA0 /* TopicsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsCell.swift; sourceTree = ""; };
65 | B24B36C7225B98810004BCA0 /* TopicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicController.swift; sourceTree = ""; };
66 | B24B36C9225B9E070004BCA0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
67 | B24B36CC225C4D770004BCA0 /* TopicNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicNameCell.swift; sourceTree = ""; };
68 | B2536D4F22667BC6002E2509 /* SignInCaptchaCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCaptchaCell.swift; sourceTree = ""; };
69 | B2536D512266D377002E2509 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; };
70 | B27F7AEB22A7E040003D52C9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
71 | B2971DB5226972DF000C6645 /* TopicCommentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicCommentCell.swift; sourceTree = ""; };
72 | B2971DB72269BF75000C6645 /* NodesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesController.swift; sourceTree = ""; };
73 | B2971DB92269BFB5000C6645 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; };
74 | B29F7AA92265A8FE00A6E7DE /* UserController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserController.swift; sourceTree = ""; };
75 | B29F7AAC22662D3C00A6E7DE /* UserHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHeaderView.swift; sourceTree = ""; };
76 | B29F7AAE226643CA00A6E7DE /* SignInController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInController.swift; sourceTree = ""; };
77 | B2B2FEE2225E337D00E5364B /* Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = ""; };
78 | B2B2FEE4225E33F000E5364B /* TopicBodyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicBodyCell.swift; sourceTree = ""; };
79 | B2B2FEE6225E34DD00E5364B /* TopicWebCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicWebCell.swift; sourceTree = ""; };
80 | B2B2FEE8225E353F00E5364B /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; };
81 | B2B2FEEA225E3D8A00E5364B /* TopicReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicReplyCell.swift; sourceTree = ""; };
82 | B2B2FEEC225F53BC00E5364B /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; };
83 | B2B2FEEE225F53D500E5364B /* NetworkErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrorView.swift; sourceTree = ""; };
84 | B2B2FEF0225F53EB00E5364B /* NoContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoContentView.swift; sourceTree = ""; };
85 | B2B2FEF2225F622500E5364B /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; };
86 | B2CA880F226D848A00F8B12F /* TopicsNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsNodeButton.swift; sourceTree = ""; };
87 | B2CA8811226D97E900F8B12F /* TopicsNodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsNodesView.swift; sourceTree = ""; };
88 | B2CA8813226DA79500F8B12F /* SignInUsernameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInUsernameCell.swift; sourceTree = ""; };
89 | B2CA8815226DA97300F8B12F /* SignInPasswordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInPasswordCell.swift; sourceTree = ""; };
90 | B2E3704922AB70980072DDCC /* ComposeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeController.swift; sourceTree = ""; };
91 | B2E6BA7C23364E0000A5F230 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.storyboard"; sourceTree = ""; };
92 | B2E85F8E226717F100D20A3F /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; };
93 | /* End PBXFileReference section */
94 |
95 | /* Begin PBXFrameworksBuildPhase section */
96 | B24B36A0225A27210004BCA0 /* Frameworks */ = {
97 | isa = PBXFrameworksBuildPhase;
98 | buildActionMask = 2147483647;
99 | files = (
100 | 95B6016E29031068964A3358 /* Pods_V2EX.framework in Frameworks */,
101 | );
102 | runOnlyForDeploymentPostprocessing = 0;
103 | };
104 | /* End PBXFrameworksBuildPhase section */
105 |
106 | /* Begin PBXGroup section */
107 | 5CBD6359BFF72C505743A6B5 /* Pods */ = {
108 | isa = PBXGroup;
109 | children = (
110 | 2E0AC9774B17E9B73450E858 /* Pods-V2EX.debug.xcconfig */,
111 | 76CB0DEDDE91A97AC1E5014E /* Pods-V2EX.release.xcconfig */,
112 | );
113 | path = Pods;
114 | sourceTree = "";
115 | };
116 | B24B369A225A27210004BCA0 = {
117 | isa = PBXGroup;
118 | children = (
119 | B24B36A5225A27210004BCA0 /* V2EX */,
120 | B24B36A4225A27210004BCA0 /* Products */,
121 | 5CBD6359BFF72C505743A6B5 /* Pods */,
122 | C7368866D6E4DA588E1B7498 /* Frameworks */,
123 | );
124 | sourceTree = "";
125 | };
126 | B24B36A4225A27210004BCA0 /* Products */ = {
127 | isa = PBXGroup;
128 | children = (
129 | B24B36A3225A27210004BCA0 /* V2EX.app */,
130 | );
131 | name = Products;
132 | sourceTree = "";
133 | };
134 | B24B36A5225A27210004BCA0 /* V2EX */ = {
135 | isa = PBXGroup;
136 | children = (
137 | B24B36A6225A27210004BCA0 /* AppDelegate.swift */,
138 | B24B36AD225A27220004BCA0 /* Assets.xcassets */,
139 | B24B36B8225A29EA0004BCA0 /* Classes */,
140 | B27F7AEB22A7E040003D52C9 /* GoogleService-Info.plist */,
141 | B24B36B2225A27220004BCA0 /* Info.plist */,
142 | B24B36AF225A27220004BCA0 /* LaunchScreen.storyboard */,
143 | );
144 | path = V2EX;
145 | sourceTree = "";
146 | };
147 | B24B36B8225A29EA0004BCA0 /* Classes */ = {
148 | isa = PBXGroup;
149 | children = (
150 | B24B36B9225A29F10004BCA0 /* Controllers */,
151 | B24B36BC225B3DDF0004BCA0 /* Models */,
152 | B24B36C3225B65900004BCA0 /* Views */,
153 | );
154 | path = Classes;
155 | sourceTree = "";
156 | };
157 | B24B36B9225A29F10004BCA0 /* Controllers */ = {
158 | isa = PBXGroup;
159 | children = (
160 | B2E3704922AB70980072DDCC /* ComposeController.swift */,
161 | B23B1E3E22B7AEED005C7931 /* MoreController.swift */,
162 | B2971DB72269BF75000C6645 /* NodesController.swift */,
163 | B29F7AAE226643CA00A6E7DE /* SignInController.swift */,
164 | B24B36C7225B98810004BCA0 /* TopicController.swift */,
165 | B24B36BA225A2A070004BCA0 /* TopicsController.swift */,
166 | B29F7AA92265A8FE00A6E7DE /* UserController.swift */,
167 | B24B36C9225B9E070004BCA0 /* ViewController.swift */,
168 | B2B2FEF2225F622500E5364B /* WebViewController.swift */,
169 | );
170 | path = Controllers;
171 | sourceTree = "";
172 | };
173 | B24B36BC225B3DDF0004BCA0 /* Models */ = {
174 | isa = PBXGroup;
175 | children = (
176 | B2E85F8E226717F100D20A3F /* Comment.swift */,
177 | B24B36BF225B60EF0004BCA0 /* Node.swift */,
178 | B2B2FEE2225E337D00E5364B /* Reply.swift */,
179 | B2971DB92269BFB5000C6645 /* Section.swift */,
180 | B2536D512266D377002E2509 /* Session.swift */,
181 | B24B36BD225B3DF20004BCA0 /* Topic.swift */,
182 | B24B36C1225B61190004BCA0 /* User.swift */,
183 | );
184 | path = Models;
185 | sourceTree = "";
186 | };
187 | B24B36C3225B65900004BCA0 /* Views */ = {
188 | isa = PBXGroup;
189 | children = (
190 | B2B2FEEC225F53BC00E5364B /* ActivityIndicatorView.swift */,
191 | B2B2FEEE225F53D500E5364B /* NetworkErrorView.swift */,
192 | B2B2FEF0225F53EB00E5364B /* NoContentView.swift */,
193 | B2536D4E22667BAD002E2509 /* SignIn */,
194 | B24B36CB225C4D680004BCA0 /* Topic */,
195 | B24B36C4225B659C0004BCA0 /* Topics */,
196 | B227C89B22A6B55400C19F41 /* UIImageView.swift */,
197 | B2B2FEE8225E353F00E5364B /* UIView.swift */,
198 | B29F7AAB22662D2900A6E7DE /* User */,
199 | );
200 | path = Views;
201 | sourceTree = "";
202 | };
203 | B24B36C4225B659C0004BCA0 /* Topics */ = {
204 | isa = PBXGroup;
205 | children = (
206 | B24B36C5225B65A60004BCA0 /* TopicsCell.swift */,
207 | B2CA880F226D848A00F8B12F /* TopicsNodeButton.swift */,
208 | B2CA8811226D97E900F8B12F /* TopicsNodesView.swift */,
209 | );
210 | path = Topics;
211 | sourceTree = "";
212 | };
213 | B24B36CB225C4D680004BCA0 /* Topic */ = {
214 | isa = PBXGroup;
215 | children = (
216 | B2B2FEE4225E33F000E5364B /* TopicBodyCell.swift */,
217 | B2971DB5226972DF000C6645 /* TopicCommentCell.swift */,
218 | B24B36CC225C4D770004BCA0 /* TopicNameCell.swift */,
219 | B2B2FEEA225E3D8A00E5364B /* TopicReplyCell.swift */,
220 | B2B2FEE6225E34DD00E5364B /* TopicWebCell.swift */,
221 | );
222 | path = Topic;
223 | sourceTree = "";
224 | };
225 | B2536D4E22667BAD002E2509 /* SignIn */ = {
226 | isa = PBXGroup;
227 | children = (
228 | B2536D4F22667BC6002E2509 /* SignInCaptchaCell.swift */,
229 | B2CA8815226DA97300F8B12F /* SignInPasswordCell.swift */,
230 | B2CA8813226DA79500F8B12F /* SignInUsernameCell.swift */,
231 | );
232 | path = SignIn;
233 | sourceTree = "";
234 | };
235 | B29F7AAB22662D2900A6E7DE /* User */ = {
236 | isa = PBXGroup;
237 | children = (
238 | B29F7AAC22662D3C00A6E7DE /* UserHeaderView.swift */,
239 | );
240 | path = User;
241 | sourceTree = "";
242 | };
243 | C7368866D6E4DA588E1B7498 /* Frameworks */ = {
244 | isa = PBXGroup;
245 | children = (
246 | 507B5C8E95694E8A84014586 /* Pods_V2EX.framework */,
247 | );
248 | name = Frameworks;
249 | sourceTree = "";
250 | };
251 | /* End PBXGroup section */
252 |
253 | /* Begin PBXNativeTarget section */
254 | B24B36A2225A27210004BCA0 /* V2EX */ = {
255 | isa = PBXNativeTarget;
256 | buildConfigurationList = B24B36B5225A27220004BCA0 /* Build configuration list for PBXNativeTarget "V2EX" */;
257 | buildPhases = (
258 | C65A1F9486CA18818E7B513C /* [CP] Check Pods Manifest.lock */,
259 | B24B369F225A27210004BCA0 /* Sources */,
260 | B24B36A0225A27210004BCA0 /* Frameworks */,
261 | B24B36A1225A27210004BCA0 /* Resources */,
262 | 4CE3C8611EF3860AA8F66B11 /* [CP] Embed Pods Frameworks */,
263 | B2AF991D226A10D400454882 /* ShellScript */,
264 | );
265 | buildRules = (
266 | );
267 | dependencies = (
268 | );
269 | name = V2EX;
270 | productName = V2EX;
271 | productReference = B24B36A3225A27210004BCA0 /* V2EX.app */;
272 | productType = "com.apple.product-type.application";
273 | };
274 | /* End PBXNativeTarget section */
275 |
276 | /* Begin PBXProject section */
277 | B24B369B225A27210004BCA0 /* Project object */ = {
278 | isa = PBXProject;
279 | attributes = {
280 | LastSwiftUpdateCheck = 1020;
281 | LastUpgradeCheck = 1020;
282 | ORGANIZATIONNAME = "Jianqiu Xiao";
283 | TargetAttributes = {
284 | B24B36A2225A27210004BCA0 = {
285 | CreatedOnToolsVersion = 10.2;
286 | };
287 | };
288 | };
289 | buildConfigurationList = B24B369E225A27210004BCA0 /* Build configuration list for PBXProject "V2EX" */;
290 | compatibilityVersion = "Xcode 9.3";
291 | developmentRegion = "zh-Hans";
292 | hasScannedForEncodings = 0;
293 | knownRegions = (
294 | "zh-Hans",
295 | );
296 | mainGroup = B24B369A225A27210004BCA0;
297 | productRefGroup = B24B36A4225A27210004BCA0 /* Products */;
298 | projectDirPath = "";
299 | projectRoot = "";
300 | targets = (
301 | B24B36A2225A27210004BCA0 /* V2EX */,
302 | );
303 | };
304 | /* End PBXProject section */
305 |
306 | /* Begin PBXResourcesBuildPhase section */
307 | B24B36A1225A27210004BCA0 /* Resources */ = {
308 | isa = PBXResourcesBuildPhase;
309 | buildActionMask = 2147483647;
310 | files = (
311 | B24B36B1225A27220004BCA0 /* LaunchScreen.storyboard in Resources */,
312 | B24B36AE225A27220004BCA0 /* Assets.xcassets in Resources */,
313 | B27F7AEC22A7E040003D52C9 /* GoogleService-Info.plist in Resources */,
314 | );
315 | runOnlyForDeploymentPostprocessing = 0;
316 | };
317 | /* End PBXResourcesBuildPhase section */
318 |
319 | /* Begin PBXShellScriptBuildPhase section */
320 | 4CE3C8611EF3860AA8F66B11 /* [CP] Embed Pods Frameworks */ = {
321 | isa = PBXShellScriptBuildPhase;
322 | buildActionMask = 2147483647;
323 | files = (
324 | );
325 | inputFileListPaths = (
326 | "${PODS_ROOT}/Target Support Files/Pods-V2EX/Pods-V2EX-frameworks-${CONFIGURATION}-input-files.xcfilelist",
327 | );
328 | name = "[CP] Embed Pods Frameworks";
329 | outputFileListPaths = (
330 | "${PODS_ROOT}/Target Support Files/Pods-V2EX/Pods-V2EX-frameworks-${CONFIGURATION}-output-files.xcfilelist",
331 | );
332 | runOnlyForDeploymentPostprocessing = 0;
333 | shellPath = /bin/sh;
334 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-V2EX/Pods-V2EX-frameworks.sh\"\n";
335 | showEnvVarsInLog = 0;
336 | };
337 | B2AF991D226A10D400454882 /* ShellScript */ = {
338 | isa = PBXShellScriptBuildPhase;
339 | buildActionMask = 2147483647;
340 | files = (
341 | );
342 | inputFileListPaths = (
343 | );
344 | inputPaths = (
345 | );
346 | outputFileListPaths = (
347 | );
348 | outputPaths = (
349 | );
350 | runOnlyForDeploymentPostprocessing = 0;
351 | shellPath = /bin/sh;
352 | shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n";
353 | };
354 | C65A1F9486CA18818E7B513C /* [CP] Check Pods Manifest.lock */ = {
355 | isa = PBXShellScriptBuildPhase;
356 | buildActionMask = 2147483647;
357 | files = (
358 | );
359 | inputFileListPaths = (
360 | );
361 | inputPaths = (
362 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
363 | "${PODS_ROOT}/Manifest.lock",
364 | );
365 | name = "[CP] Check Pods Manifest.lock";
366 | outputFileListPaths = (
367 | );
368 | outputPaths = (
369 | "$(DERIVED_FILE_DIR)/Pods-V2EX-checkManifestLockResult.txt",
370 | );
371 | runOnlyForDeploymentPostprocessing = 0;
372 | shellPath = /bin/sh;
373 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
374 | showEnvVarsInLog = 0;
375 | };
376 | /* End PBXShellScriptBuildPhase section */
377 |
378 | /* Begin PBXSourcesBuildPhase section */
379 | B24B369F225A27210004BCA0 /* Sources */ = {
380 | isa = PBXSourcesBuildPhase;
381 | buildActionMask = 2147483647;
382 | files = (
383 | B2B2FEEF225F53D500E5364B /* NetworkErrorView.swift in Sources */,
384 | B2B2FEF3225F622500E5364B /* WebViewController.swift in Sources */,
385 | B29F7AAA2265A8FE00A6E7DE /* UserController.swift in Sources */,
386 | B2B2FEE9225E353F00E5364B /* UIView.swift in Sources */,
387 | B24B36CD225C4D770004BCA0 /* TopicNameCell.swift in Sources */,
388 | B23B1E3F22B7AEED005C7931 /* MoreController.swift in Sources */,
389 | B2B2FEEB225E3D8A00E5364B /* TopicReplyCell.swift in Sources */,
390 | B24B36A7225A27210004BCA0 /* AppDelegate.swift in Sources */,
391 | B29F7AAD22662D3C00A6E7DE /* UserHeaderView.swift in Sources */,
392 | B2971DB82269BF75000C6645 /* NodesController.swift in Sources */,
393 | B2971DBA2269BFB5000C6645 /* Section.swift in Sources */,
394 | B2B2FEF1225F53EB00E5364B /* NoContentView.swift in Sources */,
395 | B24B36BE225B3DF20004BCA0 /* Topic.swift in Sources */,
396 | B2E85F8F226717F100D20A3F /* Comment.swift in Sources */,
397 | B24B36BB225A2A070004BCA0 /* TopicsController.swift in Sources */,
398 | B2CA8816226DA97300F8B12F /* SignInPasswordCell.swift in Sources */,
399 | B2B2FEED225F53BC00E5364B /* ActivityIndicatorView.swift in Sources */,
400 | B2B2FEE5225E33F000E5364B /* TopicBodyCell.swift in Sources */,
401 | B2CA8812226D97E900F8B12F /* TopicsNodesView.swift in Sources */,
402 | B24B36C2225B61190004BCA0 /* User.swift in Sources */,
403 | B2536D5022667BC6002E2509 /* SignInCaptchaCell.swift in Sources */,
404 | B24B36C0225B60EF0004BCA0 /* Node.swift in Sources */,
405 | B2CA8814226DA79500F8B12F /* SignInUsernameCell.swift in Sources */,
406 | B2CA8810226D848A00F8B12F /* TopicsNodeButton.swift in Sources */,
407 | B2B2FEE3225E337D00E5364B /* Reply.swift in Sources */,
408 | B24B36C8225B98810004BCA0 /* TopicController.swift in Sources */,
409 | B24B36CA225B9E070004BCA0 /* ViewController.swift in Sources */,
410 | B2971DB6226972DF000C6645 /* TopicCommentCell.swift in Sources */,
411 | B227C89C22A6B55400C19F41 /* UIImageView.swift in Sources */,
412 | B2536D522266D377002E2509 /* Session.swift in Sources */,
413 | B2E3704A22AB70980072DDCC /* ComposeController.swift in Sources */,
414 | B2B2FEE7225E34DD00E5364B /* TopicWebCell.swift in Sources */,
415 | B24B36C6225B65A60004BCA0 /* TopicsCell.swift in Sources */,
416 | B29F7AAF226643CA00A6E7DE /* SignInController.swift in Sources */,
417 | );
418 | runOnlyForDeploymentPostprocessing = 0;
419 | };
420 | /* End PBXSourcesBuildPhase section */
421 |
422 | /* Begin PBXVariantGroup section */
423 | B24B36AF225A27220004BCA0 /* LaunchScreen.storyboard */ = {
424 | isa = PBXVariantGroup;
425 | children = (
426 | B2E6BA7C23364E0000A5F230 /* zh-Hans */,
427 | );
428 | name = LaunchScreen.storyboard;
429 | sourceTree = "";
430 | };
431 | /* End PBXVariantGroup section */
432 |
433 | /* Begin XCBuildConfiguration section */
434 | B24B36B3225A27220004BCA0 /* Debug */ = {
435 | isa = XCBuildConfiguration;
436 | buildSettings = {
437 | ALWAYS_SEARCH_USER_PATHS = NO;
438 | CLANG_ANALYZER_NONNULL = YES;
439 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
440 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
441 | CLANG_CXX_LIBRARY = "libc++";
442 | CLANG_ENABLE_MODULES = YES;
443 | CLANG_ENABLE_OBJC_ARC = YES;
444 | CLANG_ENABLE_OBJC_WEAK = YES;
445 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
446 | CLANG_WARN_BOOL_CONVERSION = YES;
447 | CLANG_WARN_COMMA = YES;
448 | CLANG_WARN_CONSTANT_CONVERSION = YES;
449 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
450 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
451 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
452 | CLANG_WARN_EMPTY_BODY = YES;
453 | CLANG_WARN_ENUM_CONVERSION = YES;
454 | CLANG_WARN_INFINITE_RECURSION = YES;
455 | CLANG_WARN_INT_CONVERSION = YES;
456 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
457 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
458 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
459 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
460 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
461 | CLANG_WARN_STRICT_PROTOTYPES = YES;
462 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
463 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
464 | CLANG_WARN_UNREACHABLE_CODE = YES;
465 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
466 | CODE_SIGN_IDENTITY = "iPhone Developer";
467 | COPY_PHASE_STRIP = NO;
468 | DEBUG_INFORMATION_FORMAT = dwarf;
469 | ENABLE_STRICT_OBJC_MSGSEND = YES;
470 | ENABLE_TESTABILITY = YES;
471 | GCC_C_LANGUAGE_STANDARD = gnu11;
472 | GCC_DYNAMIC_NO_PIC = NO;
473 | GCC_NO_COMMON_BLOCKS = YES;
474 | GCC_OPTIMIZATION_LEVEL = 0;
475 | GCC_PREPROCESSOR_DEFINITIONS = (
476 | "DEBUG=1",
477 | "$(inherited)",
478 | );
479 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
480 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
481 | GCC_WARN_UNDECLARED_SELECTOR = YES;
482 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
483 | GCC_WARN_UNUSED_FUNCTION = YES;
484 | GCC_WARN_UNUSED_VARIABLE = YES;
485 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
486 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
487 | MTL_FAST_MATH = YES;
488 | ONLY_ACTIVE_ARCH = YES;
489 | SDKROOT = iphoneos;
490 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
491 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
492 | };
493 | name = Debug;
494 | };
495 | B24B36B4225A27220004BCA0 /* Release */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | ALWAYS_SEARCH_USER_PATHS = NO;
499 | CLANG_ANALYZER_NONNULL = YES;
500 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
501 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
502 | CLANG_CXX_LIBRARY = "libc++";
503 | CLANG_ENABLE_MODULES = YES;
504 | CLANG_ENABLE_OBJC_ARC = YES;
505 | CLANG_ENABLE_OBJC_WEAK = YES;
506 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
507 | CLANG_WARN_BOOL_CONVERSION = YES;
508 | CLANG_WARN_COMMA = YES;
509 | CLANG_WARN_CONSTANT_CONVERSION = YES;
510 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
511 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
512 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
513 | CLANG_WARN_EMPTY_BODY = YES;
514 | CLANG_WARN_ENUM_CONVERSION = YES;
515 | CLANG_WARN_INFINITE_RECURSION = YES;
516 | CLANG_WARN_INT_CONVERSION = YES;
517 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
518 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
519 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
520 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
521 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
522 | CLANG_WARN_STRICT_PROTOTYPES = YES;
523 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
524 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
525 | CLANG_WARN_UNREACHABLE_CODE = YES;
526 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
527 | CODE_SIGN_IDENTITY = "iPhone Developer";
528 | COPY_PHASE_STRIP = NO;
529 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
530 | ENABLE_NS_ASSERTIONS = NO;
531 | ENABLE_STRICT_OBJC_MSGSEND = YES;
532 | GCC_C_LANGUAGE_STANDARD = gnu11;
533 | GCC_NO_COMMON_BLOCKS = YES;
534 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
535 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
536 | GCC_WARN_UNDECLARED_SELECTOR = YES;
537 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
538 | GCC_WARN_UNUSED_FUNCTION = YES;
539 | GCC_WARN_UNUSED_VARIABLE = YES;
540 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
541 | MTL_ENABLE_DEBUG_INFO = NO;
542 | MTL_FAST_MATH = YES;
543 | SDKROOT = iphoneos;
544 | SWIFT_COMPILATION_MODE = wholemodule;
545 | SWIFT_OPTIMIZATION_LEVEL = "-O";
546 | VALIDATE_PRODUCT = YES;
547 | };
548 | name = Release;
549 | };
550 | B24B36B6225A27220004BCA0 /* Debug */ = {
551 | isa = XCBuildConfiguration;
552 | baseConfigurationReference = 2E0AC9774B17E9B73450E858 /* Pods-V2EX.debug.xcconfig */;
553 | buildSettings = {
554 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
555 | CODE_SIGN_STYLE = Automatic;
556 | CURRENT_PROJECT_VERSION = 12;
557 | DEVELOPMENT_TEAM = 4ZT4M533K7;
558 | INFOPLIST_FILE = V2EX/Info.plist;
559 | LD_RUNPATH_SEARCH_PATHS = (
560 | "$(inherited)",
561 | "@executable_path/Frameworks",
562 | );
563 | MARKETING_VERSION = 1.4;
564 | PRODUCT_BUNDLE_IDENTIFIER = com.secipin.v2ex;
565 | PRODUCT_NAME = "$(TARGET_NAME)";
566 | SWIFT_VERSION = 5.0;
567 | TARGETED_DEVICE_FAMILY = "1,2";
568 | };
569 | name = Debug;
570 | };
571 | B24B36B7225A27220004BCA0 /* Release */ = {
572 | isa = XCBuildConfiguration;
573 | baseConfigurationReference = 76CB0DEDDE91A97AC1E5014E /* Pods-V2EX.release.xcconfig */;
574 | buildSettings = {
575 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
576 | CODE_SIGN_STYLE = Automatic;
577 | CURRENT_PROJECT_VERSION = 12;
578 | DEVELOPMENT_TEAM = 4ZT4M533K7;
579 | INFOPLIST_FILE = V2EX/Info.plist;
580 | LD_RUNPATH_SEARCH_PATHS = (
581 | "$(inherited)",
582 | "@executable_path/Frameworks",
583 | );
584 | MARKETING_VERSION = 1.4;
585 | PRODUCT_BUNDLE_IDENTIFIER = com.secipin.v2ex;
586 | PRODUCT_NAME = "$(TARGET_NAME)";
587 | SWIFT_VERSION = 5.0;
588 | TARGETED_DEVICE_FAMILY = "1,2";
589 | };
590 | name = Release;
591 | };
592 | /* End XCBuildConfiguration section */
593 |
594 | /* Begin XCConfigurationList section */
595 | B24B369E225A27210004BCA0 /* Build configuration list for PBXProject "V2EX" */ = {
596 | isa = XCConfigurationList;
597 | buildConfigurations = (
598 | B24B36B3225A27220004BCA0 /* Debug */,
599 | B24B36B4225A27220004BCA0 /* Release */,
600 | );
601 | defaultConfigurationIsVisible = 0;
602 | defaultConfigurationName = Release;
603 | };
604 | B24B36B5225A27220004BCA0 /* Build configuration list for PBXNativeTarget "V2EX" */ = {
605 | isa = XCConfigurationList;
606 | buildConfigurations = (
607 | B24B36B6225A27220004BCA0 /* Debug */,
608 | B24B36B7225A27220004BCA0 /* Release */,
609 | );
610 | defaultConfigurationIsVisible = 0;
611 | defaultConfigurationName = Release;
612 | };
613 | /* End XCConfigurationList section */
614 | };
615 | rootObject = B24B369B225A27210004BCA0 /* Project object */;
616 | }
617 |
--------------------------------------------------------------------------------