├── Resources ├── 12.jpg ├── 32.jpg ├── Media.xcassets │ ├── Contents.json │ ├── ic_close.imageset │ │ ├── ic_close.png │ │ ├── ic_close_2x.png │ │ ├── ic_close_3x.png │ │ └── Contents.json │ ├── ic_face.imageset │ │ ├── ic_face@2x.png │ │ ├── ic_face@3x.png │ │ └── Contents.json │ ├── ic_lock.imageset │ │ ├── ic_lock@2x.png │ │ ├── ic_lock@3x.png │ │ └── Contents.json │ ├── ic_send.imageset │ │ ├── ic_send@2x.png │ │ ├── ic_send@3x.png │ │ └── Contents.json │ ├── reply_n.imageset │ │ ├── reply_n@2x.png │ │ └── Contents.json │ ├── ic_cancel.imageset │ │ ├── ic_cancel@2x.png │ │ ├── ic_cancel@3x.png │ │ └── Contents.json │ ├── ic_favorite.imageset │ │ ├── ic_favorite.png │ │ ├── ic_favorite_2x.png │ │ ├── ic_favorite_3x.png │ │ └── Contents.json │ ├── ic_settings.imageset │ │ ├── ic_settings.png │ │ ├── ic_settings_2x.png │ │ ├── ic_settings_3x.png │ │ └── Contents.json │ ├── ic_vpn_key.imageset │ │ ├── ic_vpn_key.png │ │ ├── ic_vpn_key_2x.png │ │ ├── ic_vpn_key_3x.png │ │ └── Contents.json │ ├── ic_warning.imageset │ │ ├── ic_warning.png │ │ ├── ic_warning_2x.png │ │ ├── ic_warning_3x.png │ │ └── Contents.json │ ├── ic_block_48pt.imageset │ │ ├── ic_block_48pt.png │ │ ├── ic_block_48pt_2x.png │ │ ├── ic_block_48pt_3x.png │ │ └── Contents.json │ ├── ic_grade_48pt.imageset │ │ ├── ic_grade_48pt.png │ │ ├── ic_grade_48pt_2x.png │ │ ├── ic_grade_48pt_3x.png │ │ └── Contents.json │ ├── ic_menu_36pt.imageset │ │ ├── ic_menu_36pt@2x.png │ │ ├── ic_menu_36pt@3x.png │ │ └── Contents.json │ ├── ic_turned_in.imageset │ │ ├── ic_turned_in@2x.png │ │ ├── ic_turned_in@3x.png │ │ └── Contents.json │ ├── ic_visibility.imageset │ │ ├── ic_visibility.png │ │ ├── ic_visibility_2x.png │ │ ├── ic_visibility_3x.png │ │ └── Contents.json │ ├── ic_navigation.imageset │ │ ├── ic_navigation@2x.png │ │ ├── ic_navigation@3x.png │ │ └── Contents.json │ ├── ic_explore_48pt.imageset │ │ ├── ic_explore_48pt.png │ │ ├── ic_explore_48pt_2x.png │ │ ├── ic_explore_48pt_3x.png │ │ └── Contents.json │ ├── ic_favorite_48pt.imageset │ │ ├── ic_favorite_48pt.png │ │ ├── ic_favorite_48pt_2x.png │ │ ├── ic_favorite_48pt_3x.png │ │ └── Contents.json │ ├── ic_chevron_left.imageset │ │ ├── ic_chevron_left@2x.png │ │ ├── ic_chevron_left@3x.png │ │ └── Contents.json │ ├── ic_visibility_off.imageset │ │ ├── ic_visibility_off.png │ │ ├── ic_visibility_off_2x.png │ │ ├── ic_visibility_off_3x.png │ │ └── Contents.json │ ├── ic_account_circle.imageset │ │ ├── ic_account_circle@2x.png │ │ ├── ic_account_circle@3x.png │ │ └── Contents.json │ ├── ic_arrow_downward.imageset │ │ ├── ic_arrow_downward@2x.png │ │ ├── ic_arrow_downward@3x.png │ │ └── Contents.json │ ├── ic_arrow_drop_up.imageset │ │ ├── ic_arrow_drop_up@2x.png │ │ ├── ic_arrow_drop_up@3x.png │ │ └── Contents.json │ ├── ic_favorite_18pt.imageset │ │ ├── ic_favorite_18pt@2x.png │ │ ├── ic_favorite_18pt@3x.png │ │ └── Contents.json │ ├── ic_favorite_border.imageset │ │ ├── ic_favorite_border.png │ │ ├── ic_favorite_border_2x.png │ │ ├── ic_favorite_border_3x.png │ │ └── Contents.json │ ├── ic_notifications.imageset │ │ ├── ic_notifications@2x.png │ │ ├── ic_notifications@3x.png │ │ └── Contents.json │ ├── ic_speaker_notes.imageset │ │ ├── ic_speaker_notes@2x.png │ │ ├── ic_speaker_notes@3x.png │ │ └── Contents.json │ ├── ic_turned_in_not.imageset │ │ ├── ic_turned_in_not@2x.png │ │ ├── ic_turned_in_not@3x.png │ │ └── Contents.json │ ├── onepassword-button.imageset │ │ ├── onepassword-button.png │ │ ├── onepassword-button@2x.png │ │ ├── onepassword-button@3x.png │ │ └── Contents.json │ ├── ic_more_horiz_36pt.imageset │ │ ├── ic_more_horiz_36pt@2x.png │ │ ├── ic_more_horiz_36pt@3x.png │ │ └── Contents.json │ ├── ic_share_48pt.imageset │ │ ├── baseline_share_black_48pt_1x.png │ │ ├── baseline_share_black_48pt_2x.png │ │ ├── baseline_share_black_48pt_3x.png │ │ └── Contents.json │ ├── ic_supervisor_account.imageset │ │ ├── ic_supervisor_account.png │ │ ├── ic_supervisor_account_2x.png │ │ ├── ic_supervisor_account_3x.png │ │ └── Contents.json │ ├── ic_notifications_none.imageset │ │ ├── ic_notifications_none@2x.png │ │ ├── ic_notifications_none@3x.png │ │ └── Contents.json │ ├── ic_keyboard_arrow_right.imageset │ │ ├── ic_keyboard_arrow_right@2x.png │ │ ├── ic_keyboard_arrow_right@3x.png │ │ └── Contents.json │ ├── ic_settings_input_svideo.imageset │ │ ├── ic_settings_input_svideo@2x.png │ │ ├── ic_settings_input_svideo@3x.png │ │ └── Contents.json │ ├── ic_keyboard_arrow_left_36pt.imageset │ │ ├── ic_keyboard_arrow_left_36pt.png │ │ ├── ic_keyboard_arrow_left_36pt_2x.png │ │ ├── ic_keyboard_arrow_left_36pt_3x.png │ │ └── Contents.json │ ├── baseline_report_black_24pt.imageset │ │ ├── baseline_report_black_24pt_1x.png │ │ ├── baseline_report_black_24pt_2x.png │ │ ├── baseline_report_black_24pt_3x.png │ │ └── Contents.json │ └── BackgroundColor.colorset │ │ └── Contents.json └── CSS │ ├── lightStyle.css │ ├── darkStyle.css │ ├── font.css │ └── baseStyle.css ├── .github └── FUNDING.yml ├── V2ex-Swift ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── L1.png │ │ ├── Icon-20@2x.png │ │ ├── Icon-20@3x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-Small@2x.png │ │ ├── Icon-Small@3x.png │ │ ├── Icon-Small-40@2x.png │ │ ├── Icon-Small-40@3x.png │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ ├── 4.png │ │ ├── x.png │ │ ├── 3.5.png │ │ ├── 4.7.png │ │ ├── 5.5.png │ │ ├── Simulator Screen Shot - iPhone XR - 2018-09-22 at 12.39.22.png │ │ ├── Simulator Screen Shot - iPhone XS Max - 2018-09-22 at 12.38.50.png │ │ └── Contents.json ├── zh-Hans.lproj │ └── Localizable.strings ├── en.lproj │ └── Localizable.strings ├── Launch Screen.storyboard └── Info.plist ├── V2ex-Swift.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── V2ex-Swift.xcscheme ├── .gitignore ├── V2ex-Swift.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Common ├── String+Avatar.swift ├── JSTools.js ├── SVProgressHUD+Extension.swift ├── UIView+Extension.swift ├── V2HitTestSlopButton.swift ├── V2EXSettings.swift ├── V2Client.swift ├── UIImage+Extension.swift ├── V2Response.swift ├── V2ProgressHUD.swift ├── UITableView+Extension.swift ├── V2EXMentionedBindingParser.swift ├── Request+Extension.swift ├── V2ex+Define.swift ├── V2LeftAlignedCollectionViewFlowLayout.swift ├── UIImageView+Extension.swift ├── V2UsersKeychain.swift └── V2Style.swift ├── View ├── UIButton+Extension.swift ├── HitTestSlopView.swift ├── V2SpacingLabel.swift ├── NodeTableViewCell.swift ├── NodeCollectionReusableView.swift ├── V2Slider.swift ├── LogoutTableViewCell.swift ├── FontDisplayTableViewCell.swift ├── PodCellTableViewCell.swift ├── NotificationMenuButton.swift ├── V2PhotoBrowser │ ├── V2TapDetectingImageView.swift │ ├── V2PhotoBrowserTransionPresent.swift │ ├── V2PhotoBrowserSwipeInteractiveTransition.swift │ ├── V2Photo.swift │ └── V2PhotoBrowserTransionDismiss.swift ├── V2FPSLabel.swift ├── RightNodeTableViewCell.swift ├── TopicDetailToolCell.swift ├── V2LoadingView.swift ├── V2RefreshHeader.swift ├── V2RefreshFooter.swift ├── LeftUserHeadCell.swift ├── FontSizeSliderTableViewCell.swift ├── MemberHeaderCell.swift ├── LeftNodeTableViewCell.swift ├── AccountListTableViewCell.swift └── BaseDetailTableViewCell.swift ├── Model ├── UserApi.swift ├── BaseModel.swift ├── API │ ├── TopicApi.swift │ └── TopicListApi.swift ├── NodeModel.swift ├── NotificationsModel.swift └── Moya │ └── V2EXTargetType.swift ├── Controller ├── BaseViewController.swift ├── MyCenterViewController.swift ├── CloudflareCheckingController.swift ├── AgreementViewController.swift ├── MoreViewController.swift ├── NotificationsViewController.swift ├── WritingViewController.swift └── NodesViewController.swift ├── LICENSE ├── Podfile ├── README.md └── Podfile.lock /Resources/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/12.jpg -------------------------------------------------------------------------------- /Resources/32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/32.jpg -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Finb] 4 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/L1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/L1.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_close.imageset/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_close.imageset/ic_close.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_face.imageset/ic_face@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_face.imageset/ic_face@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_face.imageset/ic_face@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_face.imageset/ic_face@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_lock.imageset/ic_lock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_lock.imageset/ic_lock@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_lock.imageset/ic_lock@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_lock.imageset/ic_lock@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_send.imageset/ic_send@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_send.imageset/ic_send@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_send.imageset/ic_send@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_send.imageset/ic_send@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/reply_n.imageset/reply_n@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/reply_n.imageset/reply_n@2x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/4.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_close.imageset/ic_close_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_close.imageset/ic_close_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_close.imageset/ic_close_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_close.imageset/ic_close_3x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/3.5.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/4.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/4.7.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/5.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/5.5.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_cancel.imageset/ic_cancel@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_cancel.imageset/ic_cancel@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_cancel.imageset/ic_cancel@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_cancel.imageset/ic_cancel@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite.imageset/ic_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite.imageset/ic_favorite.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings.imageset/ic_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_settings.imageset/ic_settings.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_warning.imageset/ic_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_warning.imageset/ic_warning.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_vpn_key.imageset/ic_vpn_key_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_warning.imageset/ic_warning_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_warning.imageset/ic_warning_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_warning.imageset/ic_warning_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_warning.imageset/ic_warning_3x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite.imageset/ic_favorite_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite.imageset/ic_favorite_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite.imageset/ic_favorite_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite.imageset/ic_favorite_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_menu_36pt.imageset/ic_menu_36pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_menu_36pt.imageset/ic_menu_36pt@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_menu_36pt.imageset/ic_menu_36pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_menu_36pt.imageset/ic_menu_36pt@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings.imageset/ic_settings_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_settings.imageset/ic_settings_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings.imageset/ic_settings_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_settings.imageset/ic_settings_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in.imageset/ic_turned_in@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_turned_in.imageset/ic_turned_in@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in.imageset/ic_turned_in@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_turned_in.imageset/ic_turned_in@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility.imageset/ic_visibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility.imageset/ic_visibility.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_block_48pt.imageset/ic_block_48pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_grade_48pt.imageset/ic_grade_48pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_navigation.imageset/ic_navigation@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_navigation.imageset/ic_navigation@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_navigation.imageset/ic_navigation@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_navigation.imageset/ic_navigation@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility.imageset/ic_visibility_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility.imageset/ic_visibility_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility.imageset/ic_visibility_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility.imageset/ic_visibility_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_chevron_left.imageset/ic_chevron_left@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_chevron_left.imageset/ic_chevron_left@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_chevron_left.imageset/ic_chevron_left@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_chevron_left.imageset/ic_chevron_left@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_explore_48pt.imageset/ic_explore_48pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_account_circle.imageset/ic_account_circle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_account_circle.imageset/ic_account_circle@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_account_circle.imageset/ic_account_circle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_account_circle.imageset/ic_account_circle@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_downward.imageset/ic_arrow_downward@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_arrow_downward.imageset/ic_arrow_downward@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_downward.imageset/ic_arrow_downward@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_arrow_downward.imageset/ic_arrow_downward@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_drop_up.imageset/ic_arrow_drop_up@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_arrow_drop_up.imageset/ic_arrow_drop_up@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_drop_up.imageset/ic_arrow_drop_up@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_arrow_drop_up.imageset/ic_arrow_drop_up@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_18pt.imageset/ic_favorite_18pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_18pt.imageset/ic_favorite_18pt@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_18pt.imageset/ic_favorite_18pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_18pt.imageset/ic_favorite_18pt@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_48pt.imageset/ic_favorite_48pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications.imageset/ic_notifications@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_notifications.imageset/ic_notifications@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications.imageset/ic_notifications@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_notifications.imageset/ic_notifications@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_speaker_notes.imageset/ic_speaker_notes@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_speaker_notes.imageset/ic_speaker_notes@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_speaker_notes.imageset/ic_speaker_notes@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_speaker_notes.imageset/ic_speaker_notes@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in_not.imageset/ic_turned_in_not@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_turned_in_not.imageset/ic_turned_in_not@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in_not.imageset/ic_turned_in_not@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_turned_in_not.imageset/ic_turned_in_not@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_visibility_off.imageset/ic_visibility_off_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/onepassword-button.imageset/onepassword-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/onepassword-button.imageset/onepassword-button.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_favorite_border.imageset/ic_favorite_border_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_more_horiz_36pt.imageset/ic_more_horiz_36pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_more_horiz_36pt.imageset/ic_more_horiz_36pt@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_more_horiz_36pt.imageset/ic_more_horiz_36pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_more_horiz_36pt.imageset/ic_more_horiz_36pt@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/onepassword-button.imageset/onepassword-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/onepassword-button.imageset/onepassword-button@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/onepassword-button.imageset/onepassword-button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/onepassword-button.imageset/onepassword-button@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_1x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_share_48pt.imageset/baseline_share_black_48pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications_none.imageset/ic_notifications_none@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_notifications_none.imageset/ic_notifications_none@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications_none.imageset/ic_notifications_none@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_notifications_none.imageset/ic_notifications_none@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_supervisor_account.imageset/ic_supervisor_account_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings_input_svideo.imageset/ic_settings_input_svideo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_settings_input_svideo.imageset/ic_settings_input_svideo@2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings_input_svideo.imageset/ic_settings_input_svideo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_settings_input_svideo.imageset/ic_settings_input_svideo@3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_1x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/baseline_report_black_24pt.imageset/baseline_report_black_24pt_3x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt_2x.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/ic_keyboard_arrow_left_36pt_3x.png -------------------------------------------------------------------------------- /V2ex-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.mode*v* 2 | *.pbxuser 3 | *.xccheckout 4 | #*.xcbkptlist 5 | #*.xcscheme 6 | #*.xcworkspacedata 7 | *.xcuserstate 8 | build/ 9 | Pods/ 10 | .DS_Store 11 | ._.* 12 | xcuserdata 13 | DerivedData/ 14 | .idea/ 15 | iOSInjectionProject/ 16 | -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/Simulator Screen Shot - iPhone XR - 2018-09-22 at 12.39.22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/Simulator Screen Shot - iPhone XR - 2018-09-22 at 12.39.22.png -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/Simulator Screen Shot - iPhone XS Max - 2018-09-22 at 12.38.50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finb/V2ex-Swift/HEAD/V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/Simulator Screen Shot - iPhone XS Max - 2018-09-22 at 12.38.50.png -------------------------------------------------------------------------------- /Resources/CSS/lightStyle.css: -------------------------------------------------------------------------------- 1 | /* color */ 2 | 3 | a:link, a:visited, a:active { 4 | color: #778087; 5 | } 6 | body { 7 | color: #000; 8 | background-color:#FFF; 9 | } 10 | .subtle { 11 | /* background-color: #F1F2F4;*/ 12 | } 13 | .subtle .fade { 14 | color:#ADADAD; 15 | } -------------------------------------------------------------------------------- /Resources/CSS/darkStyle.css: -------------------------------------------------------------------------------- 1 | /* color */ 2 | 3 | a:link, a:visited, a:active { 4 | color: #778087; 5 | } 6 | body { 7 | color: #919191; 8 | background-color:#232226; 9 | } 10 | .subtle { 11 | /* background-color: #F1F2F4;*/ 12 | } 13 | .subtle .fade { 14 | color:#646464; 15 | } -------------------------------------------------------------------------------- /V2ex-Swift.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /V2ex-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/reply_n.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "reply_n@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_face.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_face@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_face@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_lock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_lock@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_lock@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_send.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_send@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_send@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_cancel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_cancel@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_cancel@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Common/String+Avatar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Avatar.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2020/4/9. 6 | // Copyright © 2020 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | var avatarString:String { 13 | if self.hasPrefix("http") { 14 | return self 15 | } 16 | else{ 17 | //某些时期 V2ex 使用 //: 自适应scheme ,需要加上https 18 | return "https:" + self 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_menu_36pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_menu_36pt@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_menu_36pt@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_navigation.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_navigation@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_navigation@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_turned_in@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_turned_in@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_drop_up.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_arrow_drop_up@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_arrow_drop_up@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_chevron_left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_chevron_left@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_chevron_left@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_18pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_favorite_18pt@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_favorite_18pt@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_notifications@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_notifications@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_speaker_notes.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_speaker_notes@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_speaker_notes@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_turned_in_not.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_turned_in_not@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_turned_in_not@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Common/JSTools.js: -------------------------------------------------------------------------------- 1 | function getHTMLElementAtPoint(x,y) { 2 | var tags = ""; 3 | var e = document.elementFromPoint(x,y); 4 | if (e.tagName == 'IMG') { 5 | tags += e.getAttribute('src'); 6 | } 7 | tags += ","; 8 | tags += e.width; 9 | 10 | tags += ","; 11 | tags += e.height; 12 | 13 | tags += ","; 14 | tags += e.getBoundingClientRect().left; 15 | 16 | tags += ","; 17 | tags += e.getBoundingClientRect().top; 18 | 19 | return tags; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_account_circle.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_account_circle@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_account_circle@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_arrow_downward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_arrow_downward@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_arrow_downward@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_more_horiz_36pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_more_horiz_36pt@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_more_horiz_36pt@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_close.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_close_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_close_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_notifications_none.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_notifications_none@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_notifications_none@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_keyboard_arrow_right@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_keyboard_arrow_right@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings_input_svideo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_settings_input_svideo@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic_settings_input_svideo@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_vpn_key.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_vpn_key.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_vpn_key_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_vpn_key_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_warning.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_warning.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_warning_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_warning_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_favorite.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_favorite_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_favorite_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_settings.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_settings_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_settings_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_block_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_block_48pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_block_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_block_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_grade_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_grade_48pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_grade_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_grade_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_visibility.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_visibility_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_visibility_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_explore_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_explore_48pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_explore_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_explore_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_favorite_48pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_favorite_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_favorite_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Common/SVProgressHUD+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVProgressHUD+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/5/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SVProgressHUD 11 | extension SVProgressHUD { 12 | /** 13 | 替换 SVProgressHUD 控件中弹框停留时间的计算方法,让汉字比字符停留更久的时间 14 | 不然 abcde 和 我是大帅哥 停留的时间一样, 就感觉隐藏的太快了 15 | */ 16 | func displayDurationForString(_ string:String) -> TimeInterval { 17 | return min(Double(string.utf8.count) * 0.06 + 0.5, 5.0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_favorite_border.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_favorite_border.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_favorite_border_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_favorite_border_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_visibility_off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_visibility_off.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_visibility_off_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_visibility_off_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/onepassword-button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "onepassword-button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "onepassword-button@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "onepassword-button@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/CSS/font.css: -------------------------------------------------------------------------------- 1 | /* font-size */ 2 | h1 { 3 | font-size: px; /* Default 18 */ 4 | } 5 | 6 | h2 { 7 | font-size: px; /* Default 18 */ 8 | } 9 | 10 | h3 { 11 | font-size: px; /* Default 16 */ 12 | } 13 | 14 | pre { 15 | font-size: px; /* Default 13 */ 16 | } 17 | 18 | body { 19 | font-size: px; /* Default 14 */ 20 | } 21 | .subtle { 22 | font-size : px; /* Default 12 */ 23 | } 24 | .subtle .fade { 25 | font-size : px; /* Default 10 */ 26 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_supervisor_account.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_supervisor_account.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_supervisor_account_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_supervisor_account_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_share_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_share_black_48pt_1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_share_black_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "baseline_share_black_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Common/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 16/12/14. 6 | // Copyright © 2016年 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func screenshot() -> UIImage? { 13 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0); 14 | self.drawHierarchy(in: self.bounds, afterScreenUpdates: false); 15 | let snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); 16 | UIGraphicsEndImageContext(); 17 | return snapshotImage; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/ic_keyboard_arrow_left_36pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_keyboard_arrow_left_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_keyboard_arrow_left_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_keyboard_arrow_left_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /View/UIButton+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/29/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIButton { 12 | class func roundedButton() -> UIButton { 13 | let btn = UIButton(type: .custom) 14 | btn.layer.masksToBounds = true 15 | btn.layer.cornerRadius = 3 16 | btn.backgroundColor = V2EXColor.colors.v2_ButtonBackgroundColor 17 | btn.titleLabel!.font = v2Font(14) 18 | btn.setTitleColor(UIColor.white, for: .normal) 19 | return btn 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Common/V2HitTestSlopButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2HitTestSlopButton.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2018/12/6. 6 | // Copyright © 2018 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2HitTestSlopButton: UIButton { 12 | var hitTestSlop:UIEdgeInsets = UIEdgeInsets.zero 13 | 14 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 15 | if hitTestSlop == .zero { 16 | return super.point(inside: point, with:event) 17 | } 18 | else{ 19 | return bounds.inset(by: hitTestSlop).contains(point) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /View/HitTestSlopView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HitTestSlopView.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2020/9/13. 6 | // Copyright © 2020 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HitTestSlopImageView: UIImageView { 12 | 13 | var hitTestSlop:UIEdgeInsets = UIEdgeInsets.zero 14 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 15 | if hitTestSlop == UIEdgeInsets.zero { 16 | return super.point(inside: point, with:event) 17 | } 18 | else{ 19 | return self.bounds.inset(by: hitTestSlop).contains(point) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Model/UserApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserApi.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2018/6/11. 6 | // Copyright © 2018 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum UserApi { 12 | case getUserInfo(username:String) 13 | } 14 | 15 | extension UserApi: V2EXTargetType { 16 | var path: String { 17 | switch self { 18 | case .getUserInfo: 19 | return "/api/members/show.json" 20 | } 21 | } 22 | 23 | var parameters: [String : Any]? { 24 | switch self { 25 | case let .getUserInfo(username): 26 | return ["username": username] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/baseline_report_black_24pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_report_black_24pt_1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_report_black_24pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "baseline_report_black_24pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Common/V2EXSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2EXSettings.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/24/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let keyPrefix = "me.fin.V2EXSettings." 12 | 13 | class V2EXSettings: NSObject { 14 | static let sharedInstance = V2EXSettings() 15 | fileprivate override init(){ 16 | super.init() 17 | } 18 | 19 | subscript(key:String) -> T? { 20 | get { 21 | return UserDefaults.standard.object(forKey: keyPrefix + key) as? T 22 | } 23 | set { 24 | UserDefaults.standard.setValue(newValue, forKey: keyPrefix + key ) 25 | } 26 | } 27 | } 28 | 29 | let Settings = V2EXSettings.sharedInstance 30 | -------------------------------------------------------------------------------- /Model/BaseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseModel.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/13/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ObjectMapper 12 | import Ji 13 | import Moya 14 | 15 | class BaseJsonModel: Mappable { 16 | required init?(map: Map) { 17 | 18 | } 19 | func mapping(map: Map) { 20 | 21 | } 22 | } 23 | 24 | 25 | protocol BaseHtmlModelProtocol { 26 | init(rootNode:JiNode) 27 | } 28 | 29 | /// 实现这个协议的类,可用于Moya自动解析出这个类的model的对象数组 30 | protocol HtmlModelArrayProtocol { 31 | static func createModelArray(ji:Ji) -> [Any] 32 | } 33 | 34 | /// 实现这个协议的类,可用于Moya自动解析出这个类的model的对象 35 | protocol HtmlModelProtocol { 36 | static func createModel(ji:Ji) -> Any 37 | } 38 | -------------------------------------------------------------------------------- /Controller/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/2/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController { 12 | fileprivate weak var _loadView:V2LoadingView? 13 | 14 | func showLoadingView (){ 15 | 16 | self.hideLoadingView() 17 | 18 | let aloadView = V2LoadingView() 19 | aloadView.backgroundColor = self.view.backgroundColor 20 | self.view.addSubview(aloadView) 21 | aloadView.snp.makeConstraints{ (make) -> Void in 22 | make.top.right.bottom.left.equalTo(self.view) 23 | } 24 | self._loadView = aloadView 25 | } 26 | 27 | func hideLoadingView() { 28 | self._loadView?.removeFromSuperview() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Model/API/TopicApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopicApi.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2019/9/2. 6 | // Copyright © 2019 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Moya 11 | 12 | enum TopicApi { 13 | //感谢回复 14 | case thankReply(replyId:String, once:String) 15 | case thankTopic(topicId:String, once:String) 16 | } 17 | 18 | extension TopicApi: V2EXTargetType { 19 | var method: Moya.Method { 20 | switch self { 21 | case .thankReply: return .post 22 | case .thankTopic: return .post 23 | } 24 | } 25 | var parameters: [String : Any]?{ 26 | switch self { 27 | case let .thankReply( _ , once): 28 | return ["once": once] 29 | case let .thankTopic( _ , once): 30 | return ["once": once] 31 | } 32 | } 33 | var path: String { 34 | switch self { 35 | case let .thankReply(replyId, _): 36 | return "/thank/reply/\(replyId)" 37 | case let .thankTopic(replyId, _): 38 | return "/thank/topic/\(replyId)" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Feng 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 | -------------------------------------------------------------------------------- /View/V2SpacingLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2SpacingLabel.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/11/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2SpacingLabel: UILabel { 12 | var spacing :CGFloat = 3.0 13 | override var text: String?{ 14 | set{ 15 | if let len = newValue?.Lenght, len > 0 { 16 | let attributedString = NSMutableAttributedString(string: newValue!); 17 | let paragraphStyle = NSMutableParagraphStyle(); 18 | paragraphStyle.lineBreakMode=NSLineBreakMode.byTruncatingTail; 19 | paragraphStyle.lineSpacing=self.spacing; 20 | paragraphStyle.alignment=self.textAlignment; 21 | attributedString.addAttributes( 22 | [ 23 | NSAttributedString.Key.paragraphStyle:paragraphStyle 24 | ], 25 | range: NSMakeRange(0, newValue!.Lenght)); 26 | super.attributedText = attributedString; 27 | } 28 | } 29 | get{ 30 | return super.text; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /View/NodeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/2/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NodeTableViewCell: UICollectionViewCell { 12 | var textLabel:UILabel = { 13 | let label = UILabel() 14 | label.font = v2Font(15) 15 | return label 16 | }() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | 21 | self.contentView.addSubview(textLabel) 22 | 23 | textLabel.snp.remakeConstraints({ (make) -> Void in 24 | make.center.equalTo(self.contentView) 25 | }) 26 | 27 | self.themeChangedHandler = {[weak self] _ in 28 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 29 | self?.textLabel.textColor = V2EXColor.colors.v2_TopicListUserNameColor 30 | self?.textLabel.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 31 | } 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /View/NodeCollectionReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeCollectionReusableView.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 16/4/5. 6 | // Copyright © 2016年 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NodeCollectionReusableView: UICollectionReusableView { 12 | var label : UILabel = { 13 | let _label = UILabel() 14 | _label.font = v2Font(16) 15 | return _label 16 | }() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | self.addSubview(label); 21 | 22 | label.snp.makeConstraints{ (make) -> Void in 23 | make.centerY.equalTo(self) 24 | make.left.equalTo(self).offset(15) 25 | } 26 | 27 | self.themeChangedHandler = {[weak self] _ in 28 | self?.backgroundColor = V2EXColor.colors.v2_backgroundColor 29 | self?.label.textColor = V2EXColor.colors.v2_TopicListTitleColor 30 | self?.label.backgroundColor = V2EXColor.colors.v2_backgroundColor 31 | } 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /View/V2Slider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Slider.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/10/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2Slider: UISlider { 12 | var valueChanged : ( (_ value:Float) -> Void )? 13 | 14 | init(){ 15 | super.init(frame: CGRect.zero) 16 | self.minimumValue = 0 17 | self.maximumValue = 16 18 | self.value = (V2Style.sharedInstance.fontScale - 0.8 ) / 0.5 * 10 19 | self.addTarget(self, action: #selector(V2Slider.valueChanged(_:)), for: [.valueChanged]) 20 | 21 | self.themeChangedHandler = {[weak self] (style) -> Void in 22 | self?.minimumTrackTintColor = V2EXColor.colors.v2_TopicListTitleColor 23 | self?.maximumTrackTintColor = V2EXColor.colors.v2_backgroundColor 24 | } 25 | } 26 | deinit { 27 | print("deinit") 28 | } 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | @objc func valueChanged(_ sender:UISlider) { 34 | sender.value = Float(Int(sender.value)) 35 | valueChanged?(sender.value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/V2Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Client.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/15/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DrawerController 11 | 12 | class V2Client: NSObject { 13 | static let sharedInstance = V2Client() 14 | 15 | var window : UIWindow? = nil 16 | 17 | var drawerController :DrawerController? = nil 18 | var centerViewController : HomeViewController? = nil 19 | var centerNavigation : V2EXNavigationController? = nil 20 | 21 | // 当前程序中,最上层的 NavigationController 22 | var topNavigationController : UINavigationController { 23 | get{ 24 | return V2Client.getTopNavigationController(V2Client.sharedInstance.centerNavigation!) 25 | } 26 | } 27 | 28 | fileprivate class func getTopNavigationController(_ currentNavigationController:UINavigationController) -> UINavigationController { 29 | if let topNav = currentNavigationController.visibleViewController?.navigationController{ 30 | if topNav != currentNavigationController && topNav.isKind(of: UINavigationController.self){ 31 | return getTopNavigationController(topNav) 32 | } 33 | } 34 | return currentNavigationController 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/BackgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "display-p3", 11 | "components" : { 12 | "red" : "244", 13 | "alpha" : "1.000", 14 | "blue" : "246", 15 | "green" : "245" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "light" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "display-p3", 29 | "components" : { 30 | "red" : "0.957", 31 | "alpha" : "1.000", 32 | "blue" : "0.965", 33 | "green" : "0.961" 34 | } 35 | } 36 | }, 37 | { 38 | "idiom" : "universal", 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "color" : { 46 | "color-space" : "display-p3", 47 | "components" : { 48 | "red" : "32", 49 | "alpha" : "1.000", 50 | "blue" : "35", 51 | "green" : "31" 52 | } 53 | } 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /Common/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/3/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | func roundedCornerImageWithCornerRadius(_ cornerRadius:CGFloat) -> UIImage { 14 | 15 | let w = self.size.width 16 | let h = self.size.height 17 | 18 | var targetCornerRadius = cornerRadius 19 | if cornerRadius < 0 { 20 | targetCornerRadius = 0 21 | } 22 | if cornerRadius > min(w, h) { 23 | targetCornerRadius = min(w,h) 24 | } 25 | 26 | let imageFrame = CGRect(x: 0, y: 0, width: w, height: h) 27 | UIGraphicsBeginImageContextWithOptions(self.size, false, UIScreen.main.scale) 28 | 29 | UIBezierPath(roundedRect: imageFrame, cornerRadius: targetCornerRadius).addClip() 30 | self.draw(in: imageFrame) 31 | 32 | let image = UIGraphicsGetImageFromCurrentImageContext() 33 | UIGraphicsEndImageContext() 34 | 35 | return image! 36 | } 37 | 38 | class func imageUsedTemplateMode(_ named:String) -> UIImage? { 39 | let image = UIImage(named: named) 40 | if image == nil { 41 | return nil 42 | } 43 | return image!.withRenderingMode(.alwaysTemplate) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Controller/MyCenterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyCenterViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/7/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MyCenterViewController: MemberViewController { 12 | var settingsButton:UIButton? 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | self.settingsButton = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) 17 | self.settingsButton!.contentMode = .center 18 | self.settingsButton!.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -20) 19 | self.settingsButton!.setImage(UIImage.imageUsedTemplateMode("ic_supervisor_account")!.withRenderingMode(.alwaysTemplate), for: .normal) 20 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: self.settingsButton!) 21 | self.settingsButton!.addTarget(self, action: #selector(MyCenterViewController.accountManagerClick), for: .touchUpInside) 22 | self.settingsButton!.isHidden = true 23 | } 24 | 25 | override func getDataSuccessfully(_ aModel: MemberModel) { 26 | super.getDataSuccessfully(aModel) 27 | self.settingsButton!.isHidden = false 28 | } 29 | 30 | @objc func accountManagerClick(){ 31 | self.navigationController?.pushViewController(AccountsManagerViewController(), animated: true) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /View/LogoutTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/12/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LogoutTableViewCell: UITableViewCell { 12 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 13 | super.init(style: style, reuseIdentifier: reuseIdentifier); 14 | self.setup(); 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | func setup()->Void{ 21 | 22 | 23 | self.textLabel!.text = NSLocalizedString("logOut") 24 | self.textLabel!.textAlignment = .center 25 | 26 | let separator = UIImageView() 27 | self.contentView.addSubview(separator) 28 | separator.snp.makeConstraints{ (make) -> Void in 29 | make.left.equalTo(self.contentView) 30 | make.right.bottom.equalTo(self.contentView) 31 | make.height.equalTo(SEPARATOR_HEIGHT) 32 | } 33 | self.themeChangedHandler = {[weak self] _ in 34 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 35 | self?.textLabel!.textColor = V2EXColor.colors.v2_NoticePointColor 36 | separator.image = createImageWithColor(V2EXColor.colors.v2_SeparatorColor) 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-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-Small-40@3x.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" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "L1.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /View/FontDisplayTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontDisplayTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/10/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FontDisplayTableViewCell: BaseDetailTableViewCell { 12 | 13 | override func setup()->Void{ 14 | super.setup() 15 | self.detailMarkHidden = true 16 | self.clipsToBounds = true 17 | self.titleLabel.text = "一天,一匹小马驮着麦子去磨坊。当它驮着口袋向前跑去时,突然发现一条小河挡住了去路。小马为难了,这可怎么办呢?它向四周望了望,看见一头奶牛在河边吃草。\n\n One day, a colt took a bag of wheat to the mill. As he was running with the bag on his back, he came to a small river. The colt could not decide whether he could cross it. Looking around, he saw a cow grazing nearby." 18 | self.titleLabel.numberOfLines = 0 19 | self.titleLabel.preferredMaxLayoutWidth = SCREEN_WIDTH - 24 20 | self.titleLabel.baselineAdjustment = .none 21 | 22 | self.titleLabel.snp.remakeConstraints{ (make) -> Void in 23 | make.left.top.equalTo(self.contentView).offset(12) 24 | make.right.equalTo(self.contentView).offset(-12) 25 | make.height.lessThanOrEqualTo(self.contentView).offset(-12) 26 | } 27 | 28 | self.kvoController.observe(V2Style.sharedInstance, keyPath: "fontScale", options: [NSKeyValueObservingOptions.initial, NSKeyValueObservingOptions.new]) { (_, _, _) in 29 | self.titleLabel.font = v2ScaleFont(14) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Model/API/TopicListApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopicListApi.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2018/9/17. 6 | // Copyright © 2018 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum TopicListApi { 12 | //获取首页列表 13 | case topicList(tab: String?, page: Int) 14 | //获取我的收藏帖子列表 15 | case favoriteList(page: Int) 16 | //获取节点主题列表 17 | case nodeTopicList(nodeName: String, page:Int) 18 | } 19 | 20 | extension TopicListApi: V2EXTargetType { 21 | var parameters: [String : Any]? { 22 | switch self { 23 | case let .topicList(tab, page): 24 | if tab == "all" && page > 0 { 25 | //只有全部分类能翻页 26 | return ["p": page] 27 | } 28 | return ["tab": tab ?? "all"] 29 | case let .favoriteList(page): 30 | return ["p": page] 31 | case let .nodeTopicList(_, page): 32 | return ["p": page] 33 | // default: 34 | // return nil 35 | } 36 | } 37 | 38 | var path: String { 39 | switch self { 40 | case let .topicList(tab, page): 41 | if tab == "all" && page > 0 { 42 | return "/recent" 43 | } 44 | return "/" 45 | case .favoriteList: 46 | return "/my/topics" 47 | case let .nodeTopicList(nodeName, _): 48 | return "/go/\(nodeName)" 49 | // default: 50 | // return "" 51 | } 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Common/V2Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Response.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/23/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum ErrorCode:Int { 12 | case none = 0 13 | case twoFA ; 14 | } 15 | 16 | class V2Response: NSObject { 17 | var success:Bool = false 18 | var message:String = "No message" 19 | init(success:Bool,message:String?) { 20 | super.init() 21 | self.success = success 22 | if let message = message{ 23 | self.message = message 24 | } 25 | } 26 | init(success:Bool) { 27 | super.init() 28 | self.success = success 29 | } 30 | } 31 | 32 | class V2ValueResponse: V2Response { 33 | var value:T? 34 | var code:ErrorCode = .none 35 | 36 | override init(success: Bool) { 37 | super.init(success: success) 38 | } 39 | 40 | override init(success:Bool,message:String?) { 41 | super.init(success:success) 42 | if let message = message { 43 | self.message = message 44 | } 45 | } 46 | convenience init(value:T,success:Bool) { 47 | self.init(success: success) 48 | self.value = value 49 | } 50 | convenience init(value:T,success:Bool,message:String? = nil, code:ErrorCode = .none) { 51 | self.init(value:value,success:success) 52 | if let message = message { 53 | self.message = message 54 | } 55 | self.code = code 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Common/V2ProgressHUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2ProgressHUD.swift 3 | // V2ex-Swift 4 | // 5 | // Created by skyline on 16/3/29. 6 | // Copyright © 2016年 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SVProgressHUD 11 | 12 | open class V2ProgressHUD: NSObject { 13 | open class func show() { 14 | SVProgressHUD.show(with: .none) 15 | } 16 | 17 | open class func showWithClearMask() { 18 | SVProgressHUD.show(with: .clear) 19 | } 20 | 21 | open class func dismiss() { 22 | SVProgressHUD.dismiss() 23 | } 24 | 25 | open class func showWithStatus(_ status:String!) { 26 | SVProgressHUD.show(withStatus: status) 27 | } 28 | 29 | open class func success(_ status:String!) { 30 | SVProgressHUD.showSuccess(withStatus: status) 31 | } 32 | 33 | open class func error(_ status:String!) { 34 | SVProgressHUD.showError(withStatus: status) 35 | } 36 | 37 | open class func inform(_ status:String!) { 38 | SVProgressHUD.showInfo(withStatus: status) 39 | } 40 | } 41 | 42 | public func V2Success(_ status:String!) { 43 | V2ProgressHUD.success(status) 44 | } 45 | 46 | public func V2Error(_ status:String!) { 47 | V2ProgressHUD.error(status) 48 | } 49 | 50 | public func V2Inform(_ status:String!) { 51 | V2ProgressHUD.inform(status) 52 | } 53 | 54 | public func V2BeginLoading() { 55 | V2ProgressHUD.show() 56 | } 57 | 58 | public func V2BeginLoadingWithStatus(_ status:String!) { 59 | V2ProgressHUD.showWithStatus(status) 60 | } 61 | 62 | public func V2EndLoading() { 63 | V2ProgressHUD.dismiss() 64 | } 65 | -------------------------------------------------------------------------------- /Common/UITableView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/8/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | public var Lenght:Int { 13 | get{ 14 | return self.count; 15 | } 16 | } 17 | } 18 | 19 | 20 | /** 21 | 向tableView 注册 UITableViewCell 22 | 23 | - parameter tableView: tableView 24 | - parameter cell: 要注册的类名 25 | */ 26 | func regClass(_ tableView:UITableView , cell:AnyClass)->Void { 27 | tableView.register( cell, forCellReuseIdentifier: "\(cell)"); 28 | } 29 | /** 30 | 从tableView缓存中取出对应类型的Cell 31 | 如果缓存中没有,则重新创建一个 32 | 33 | - parameter tableView: tableView 34 | - parameter cell: 要返回的Cell类型 35 | - parameter indexPath: 位置 36 | 37 | - returns: 传入Cell类型的 实例对象 38 | */ 39 | func getCell(_ tableView:UITableView ,cell: T.Type ,indexPath:IndexPath) -> T { 40 | return tableView.dequeueReusableCell(withIdentifier: "\(cell)", for: indexPath) as! T ; 41 | } 42 | 43 | extension UITableView { 44 | func v2_scrollToBottom() { 45 | let section = self.numberOfSections - 1 46 | let row = self.numberOfRows(inSection: section) - 1 47 | if section < 0 || row < 0 { 48 | return 49 | } 50 | let path = IndexPath(row: row, section: section) 51 | self.scrollToRow(at: path, at: .top, animated: false) 52 | } 53 | func v2_scrollToTop() { 54 | self.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /View/PodCellTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodCellTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/23/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PodCellTableViewCell: BaseDetailTableViewCell { 12 | 13 | var descriptionLabel:UILabel = { 14 | let label = V2SpacingLabel() 15 | label.font = v2Font(13) 16 | label.numberOfLines = 0 17 | label.preferredMaxLayoutWidth = SCREEN_WIDTH - 42 18 | label.textColor = V2EXColor.colors.v2_TopicListUserNameColor 19 | return label 20 | }() 21 | 22 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 23 | super.init(style: style, reuseIdentifier: reuseIdentifier) 24 | self.backgroundColor = V2EXColor.colors.v2_backgroundColor 25 | self.contentView.addSubview(self.descriptionLabel) 26 | self.setupLayout() 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | fileprivate func setupLayout() { 34 | self.titleLabel.snp.remakeConstraints{ (make) -> Void in 35 | make.left.top.equalTo(self.contentView).offset(12) 36 | } 37 | self.descriptionLabel.snp.makeConstraints{ (make) -> Void in 38 | make.left.equalTo(self.titleLabel) 39 | make.right.equalTo(self.contentView).offset(-30) 40 | make.top.equalTo(self.titleLabel.snp.bottom) 41 | make.bottom.equalTo(self.contentView).offset(-8); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /V2ex-Swift/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | V2ex-Swift 4 | 5 | Created by huangfeng on 1/14/16. 6 | Copyright © 2016 Fin. All rights reserved. 7 | */ 8 | //右侧边栏 9 | tech = "技术"; 10 | creative = "创意"; 11 | play = "好玩"; 12 | apple = "Apple"; 13 | jobs = "酷工作"; 14 | deals = "交易"; 15 | city = "城市"; 16 | qna = "问与答"; 17 | hot = "最热"; 18 | all = "全部"; 19 | r2 = "R2"; 20 | nodes = "节点"; 21 | members = "关注"; 22 | 23 | //左侧边栏 24 | more = "更多"; 25 | nodes = "节点"; 26 | favorites = "我的收藏"; 27 | notifications = "消息提醒"; 28 | me = "个人中心"; 29 | 30 | //用户中心 31 | posts = "创建的主题"; 32 | comments = "创建的回复"; 33 | 34 | //账户设置 35 | accounts = "账户"; 36 | current = "正在使用"; 37 | logOut ="注销当前账号"; 38 | 39 | //更多 40 | viewOptions = "阅读设置"; 41 | clearCache = "清空缓存"; 42 | 43 | rateV2ex = "去商店评分"; 44 | reportAProblem = "提出BUG或改进"; 45 | 46 | followThisProjectSourceCode = "关注本项目源代码"; 47 | open-SourceLibraries = "开源库"; 48 | version = "版本号"; 49 | 50 | //阅读设置 51 | viewOptionThemeSet = " 配色 - 点击下面选项,设置APP的配色方案"; 52 | followSystem = "跟随系统"; 53 | default = "亮色"; 54 | dark = "暗色"; 55 | 56 | 57 | viewOptionTextSize = " 文字大小 - 滑动滑块调整文字大小"; 58 | 59 | //节点导航 60 | navigation = "节点导航"; 61 | 62 | //通知中心 63 | reply = "回复"; 64 | 65 | //帖子详情 66 | postDetails = "帖子详情"; 67 | favorite = "收藏"; 68 | ignore = "忽略"; 69 | thank = "感谢"; 70 | share = "分享"; 71 | reply2 = "回 复"; 72 | cancel2 = "取 消"; 73 | 74 | userAgreement = "用户协议"; 75 | reportNude = "低俗色情"; 76 | reportHate = "仇恨言论"; 77 | reportViolence = "血腥暴力"; 78 | reportScam = "诈骗信息"; 79 | reportOther = "其他"; 80 | reportSuccess = "举报成功!"; 81 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform:ios,'13.0' 2 | inhibit_all_warnings! 3 | use_modular_headers! 4 | 5 | def pods 6 | pod 'SnapKit' 7 | pod 'Alamofire' 8 | pod 'ObjectMapper' 9 | pod 'AlamofireObjectMapper' 10 | pod 'Ji' 11 | pod 'DrawerController' 12 | pod 'Kingfisher', '~> 7.11.0' 13 | pod 'KeychainSwift' 14 | pod 'KVOController' 15 | pod 'YYText' 16 | pod 'FXBlurView' 17 | pod 'SVProgressHUD' 18 | pod 'MJRefresh', '~> 3.1.15.7' 19 | pod 'CXSwipeGestureRecognizer' 20 | pod '1PasswordExtension' 21 | pod 'Shimmer' 22 | pod 'FDFullscreenPopGesture' 23 | pod 'Moya/RxSwift' 24 | pod 'SwiftyJSON', '~> 4.3' 25 | end 26 | 27 | target 'V2ex-Swift' do 28 | pods 29 | post_install do |installer| 30 | installer.pods_project.targets.each do |target| 31 | # 将三方库的Deploy版本号都提到iOS11,隐藏编译过程中相关的Deprecated警告及其他警告 32 | target.build_configurations.each do |config| 33 | config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES' 34 | # https://stackoverflow.com/questions/63056454/xcode-12-deployment-target-warnings-when-using-cocoapods 35 | if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 11.0 36 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' 37 | end 38 | end 39 | if target.name == 'Ji' or target.name == 'Moya' or target.name == 'Result' 40 | target.build_configurations.each do |config| 41 | config.build_settings['SWIFT_VERSION'] = '4.2' 42 | end 43 | end 44 | if target.name == 'DrawerController' 45 | target.build_configurations.each do |config| 46 | config.build_settings['SWIFT_VERSION'] = '4.0' 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /View/NotificationMenuButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationButton.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/1/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NotificationMenuButton: UIButton { 12 | var aPointImageView:UIImageView? 13 | required init(){ 14 | super.init(frame: CGRect.zero) 15 | self.contentMode = .center 16 | self.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0) 17 | self.setImage(UIImage.imageUsedTemplateMode("ic_menu_36pt")!, for: .normal) 18 | 19 | self.aPointImageView = UIImageView() 20 | self.aPointImageView!.backgroundColor = V2EXColor.colors.v2_NoticePointColor 21 | self.aPointImageView!.layer.cornerRadius = 4 22 | self.aPointImageView!.layer.masksToBounds = true 23 | self.addSubview(self.aPointImageView!) 24 | self.aPointImageView!.snp.makeConstraints{ (make) -> Void in 25 | make.width.height.equalTo(8) 26 | make.top.equalTo(self).offset(3) 27 | make.right.equalTo(self).offset(-6) 28 | } 29 | 30 | self.kvoController.observe(V2User.sharedInstance, keyPath: "notificationCount", options: [.initial,.new]) { [weak self](cell, clien, change) -> Void in 31 | if V2User.sharedInstance.notificationCount > 0 { 32 | self?.aPointImageView!.isHidden = false 33 | } 34 | else{ 35 | self?.aPointImageView!.isHidden = true 36 | } 37 | } 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Resources/CSS/baseStyle.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-weight: 500; 3 | line-height: 140%; 4 | margin: 5px 0px 15px 0px; 5 | padding: 0px; 6 | } 7 | 8 | h2 { 9 | font-weight: 500; 10 | line-height: 100%; 11 | margin: 20px 0px 20px 0px; 12 | padding: 0px 0px 8px 0px; 13 | border-bottom: 1px solid #e2e2e2; 14 | } 15 | 16 | h3 { 17 | font-weight: 500; 18 | line-height: 100%; 19 | margin: 5px 0px 20px 0px; 20 | padding: 0px; 21 | } 22 | 23 | hr { 24 | border: none; 25 | height: 1px; 26 | margin-bottom: 1em; 27 | } 28 | 29 | pre { 30 | letter-spacing: 0.015em; 31 | line-height: 120%; 32 | padding: 0.5em; 33 | margin: 0px; 34 | white-space: pre; 35 | overflow-x: auto; 36 | overflow-y: auto; 37 | } 38 | 39 | pre a { 40 | color: inherit; 41 | text-decoration: underline; 42 | } 43 | 44 | code { 45 | padding: 1px 2px 1px 2px; 46 | border-radius: 2px; 47 | } 48 | 49 | 50 | ul { 51 | list-style: square; 52 | margin: 1em 0px 1em 1em; 53 | padding: 0px; 54 | } 55 | 56 | ul li, ol li { 57 | padding: 0px; 58 | margin: 0px; 59 | } 60 | 61 | ol { 62 | margin: 1em 0px 0em 2em; 63 | padding: 0px; 64 | } 65 | 66 | a:link, a:visited, a:active { 67 | text-decoration: none; 68 | word-break: break-all; 69 | } 70 | 71 | img { 72 | max-width: 100%; 73 | } 74 | .imgly { 75 | max-width: 100%; 76 | } 77 | /* ******************************* ******************************* */ 78 | body { 79 | font-family: 'Helvetica', monospace; 80 | -webkit-text-size-adjust: none; 81 | line-height: 1.75; 82 | word-wrap: break-word; 83 | max-height: 20em; 84 | padding: 5px; 85 | } 86 | .subtle { 87 | padding: 5px; 88 | } 89 | -------------------------------------------------------------------------------- /Common/V2EXMentionedBindingParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2EXMentionedBindingParser.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/25/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import YYText 12 | 13 | class V2EXMentionedBindingParser: NSObject ,YYTextParser{ 14 | var regex:NSRegularExpression 15 | override init() { 16 | self.regex = try! NSRegularExpression(pattern: "@(\\S+)\\s", options: [.caseInsensitive]) 17 | super.init() 18 | } 19 | 20 | func parseText(_ text: NSMutableAttributedString?, selectedRange: NSRangePointer?) -> Bool { 21 | guard let text = text else { 22 | return false; 23 | } 24 | self.regex.enumerateMatches(in: text.string, options: [.withoutAnchoringBounds], range: text.yy_rangeOfAll()) { (result, flags, stop) -> Void in 25 | if let result = result { 26 | let range = result.range 27 | if range.location == NSNotFound || range.length < 1 { 28 | return ; 29 | } 30 | 31 | if text.attribute(NSAttributedString.Key(rawValue: YYTextBindingAttributeName), at: range.location, effectiveRange: nil) != nil { 32 | return ; 33 | } 34 | 35 | let bindlingRange = NSMakeRange(range.location, range.length-1) 36 | let binding = YYTextBinding() 37 | binding.deleteConfirm = true ; 38 | text.yy_setTextBinding(binding, range: bindlingRange) 39 | text.yy_setColor(colorWith255RGB(0, g: 132, b: 255), range: bindlingRange) 40 | } 41 | } 42 | return false; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /View/V2PhotoBrowser/V2TapDetectingImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2TapDetectingImageView.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/22/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | @objc protocol V2TapDetectingImageViewDelegate { 13 | @objc optional func singleTapDetected(_ imageView:UIImageView,touch:UITouch) 14 | @objc optional func doubleTapDetected(_ imageView:UIImageView,touch:UITouch) 15 | } 16 | 17 | class V2TapDetectingImageView: AnimatedImageView { 18 | weak var tapDelegate:V2TapDetectingImageViewDelegate? 19 | init() { 20 | super.init(frame: CGRect.zero) 21 | self.isUserInteractionEnabled = true 22 | 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 30 | NSObject.cancelPreviousPerformRequests(withTarget: self) 31 | 32 | let touch = touches.first 33 | let tapCount = touch?.tapCount 34 | if let tapCount = tapCount { 35 | switch (tapCount) { 36 | case 1: 37 | self.perform(#selector(V2TapDetectingImageView.handleSingleTap(_:)), with: touch! , afterDelay: 0.3) 38 | case 2: 39 | self.handleDoubleTap(touch!) 40 | 41 | default :break; 42 | } 43 | } 44 | // 不继续传递事件了 45 | // self.nextResponder()?.touchesEnded(touches, withEvent: event) 46 | } 47 | 48 | @objc func handleSingleTap(_ touch:UITouch){ 49 | self.tapDelegate?.singleTapDetected?(self, touch: touch) 50 | } 51 | 52 | func handleDoubleTap(_ touch:UITouch){ 53 | self.tapDelegate?.doubleTapDetected?(self, touch: touch) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /V2ex-Swift/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | V2ex-Swift 4 | 5 | Created by huangfeng on 1/14/16. 6 | Copyright © 2016 Fin. All rights reserved. 7 | */ 8 | //右侧边栏 9 | tech = "Technology"; 10 | creative = "Creative"; 11 | play = "Play"; 12 | apple = "Apple"; 13 | jobs = "Jobs"; 14 | deals = "Deals"; 15 | city = "City"; 16 | qna = "Q&A"; 17 | hot = "Hot"; 18 | all = "All"; 19 | r2 = "R2"; 20 | nodes = "nodes"; 21 | members = "members"; 22 | 23 | //左侧边栏 24 | more = "More"; 25 | nodes = "Nodes"; 26 | favorites = "Favorites"; 27 | notifications = "Notifications"; 28 | me = "My Center"; 29 | 30 | //用户中心 31 | posts = "Posts"; 32 | comments = "Comments"; 33 | 34 | //账户设置 35 | accounts = "Accounts"; 36 | current = "current"; 37 | logOut ="LogOut"; 38 | 39 | //更多 40 | viewOptions = "View Options"; 41 | clearCache = "Clear Cache"; 42 | 43 | rateV2ex = "Rate V2EX"; 44 | reportAProblem = "Report a Problem"; 45 | 46 | followThisProjectSourceCode = "Source Code"; 47 | open-SourceLibraries = "Open Source Libraries"; 48 | version = "Version"; 49 | 50 | //阅读设置 51 | viewOptionThemeSet = " Theme - Click on the options below to set the theme"; 52 | followSystem = "Follow System"; 53 | default = "Light"; 54 | dark = "Dark"; 55 | viewOptionTextSize = " Text Size - Slide the slider to adjust the text size"; 56 | 57 | //节点导航 58 | navigation = "Navigation"; 59 | 60 | //通知中心 61 | reply = "Reply"; 62 | 63 | //帖子详情 64 | postDetails = "Topic Details"; 65 | favorite = "Favorite"; 66 | ignore = "Ignore"; 67 | thank = "Thank"; 68 | share = "Share"; 69 | reply2 = "Reply"; 70 | cancel2 = "Cancel"; 71 | 72 | userAgreement = "User Agreement"; 73 | reportNude = "Nudity or sexual activity"; 74 | reportHate = "hate speech or symblos"; 75 | reportViolence = "Violence or dangerrous"; 76 | reportScam = "Scam or fraud"; 77 | reportOther = "Other"; 78 | reportSuccess = "Thanks for letting us know!"; 79 | -------------------------------------------------------------------------------- /Controller/CloudflareCheckingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CloudflareCheckingController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2021/2/18. 6 | // Copyright © 2021 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | 12 | class CloudflareCheckingController: UIViewController, WKNavigationDelegate { 13 | let webView:WKWebView = WKWebView() 14 | var completion: (() -> ())? = nil 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | self.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 18 | 19 | self.webView.customUserAgent = USER_AGENT 20 | self.webView.backgroundColor = self.view.backgroundColor 21 | self.webView.navigationDelegate = self 22 | self.view.addSubview(self.webView) 23 | self.webView.snp.makeConstraints{ (make) -> Void in 24 | make.edges.equalTo(self.view) 25 | } 26 | self.webView.scrollView.contentInsetAdjustmentBehavior = .never 27 | 28 | _ = self.webView.load(URLRequest(url: URL(string: V2EXURL)!)) 29 | } 30 | 31 | 32 | // Cloudflare 检查后设置 cookies 33 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 34 | self.webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in 35 | for cookie in cookies { 36 | HTTPCookieStorage.shared.setCookie(cookie) 37 | } 38 | let LANGCookie = cookies.compactMap{ (cookie) -> HTTPCookie? in 39 | if cookie.name == "V2EX_LANG" { 40 | return cookie 41 | } 42 | return nil 43 | }.first 44 | 45 | // 有语言cookie,则证明检查通过 46 | if LANGCookie != nil { 47 | self.dismiss(animated: true) {[weak self] in 48 | self?.completion?() 49 | } 50 | } 51 | 52 | } 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /V2ex-Swift/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "2688h", 7 | "filename" : "Simulator Screen Shot - iPhone XS Max - 2018-09-22 at 12.38.50.png", 8 | "minimum-system-version" : "12.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "1792h", 16 | "filename" : "Simulator Screen Shot - iPhone XR - 2018-09-22 at 12.39.22.png", 17 | "minimum-system-version" : "12.0", 18 | "orientation" : "portrait", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "extent" : "full-screen", 23 | "idiom" : "iphone", 24 | "subtype" : "2436h", 25 | "filename" : "x.png", 26 | "minimum-system-version" : "11.0", 27 | "orientation" : "portrait", 28 | "scale" : "3x" 29 | }, 30 | { 31 | "extent" : "full-screen", 32 | "idiom" : "iphone", 33 | "subtype" : "736h", 34 | "filename" : "5.5.png", 35 | "minimum-system-version" : "8.0", 36 | "orientation" : "portrait", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "extent" : "full-screen", 41 | "idiom" : "iphone", 42 | "subtype" : "667h", 43 | "filename" : "4.7.png", 44 | "minimum-system-version" : "8.0", 45 | "orientation" : "portrait", 46 | "scale" : "2x" 47 | }, 48 | { 49 | "orientation" : "portrait", 50 | "idiom" : "iphone", 51 | "filename" : "3.5.png", 52 | "extent" : "full-screen", 53 | "minimum-system-version" : "7.0", 54 | "scale" : "2x" 55 | }, 56 | { 57 | "extent" : "full-screen", 58 | "idiom" : "iphone", 59 | "subtype" : "retina4", 60 | "filename" : "4.png", 61 | "minimum-system-version" : "7.0", 62 | "orientation" : "portrait", 63 | "scale" : "2x" 64 | } 65 | ], 66 | "info" : { 67 | "version" : 1, 68 | "author" : "xcode" 69 | } 70 | } -------------------------------------------------------------------------------- /V2ex-Swift/Launch Screen.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## V2ex-Swift 2 | This's a 3rd-party app for V2EX , designed to make V2EX reading more friendly. 3 | 4 | [![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/Finb/V2ex-Swift/master/LICENSE) 5 |
6 | ## Download 7 | 8 | 9 | 10 | 11 | ## How to build 12 | 1) Clone the repository 13 | ``` 14 | $ git clone https://github.com/Finb/V2ex-Swift.git 15 | ``` 16 | 2) Install dependencies 17 | ``` 18 | $ pod install 19 | ``` 20 | 3) Open the workspace in Xcode 21 | ``` 22 | $ open "V2ex-Swift.xcworkspace" 23 | ``` 24 | 4) Compile and run the app in your simulator or iOS device 25 | 26 | ## Requirements 27 | * Xcode 9.3 28 | * iOS 9+ 29 | * Swift 4.1 30 | * CocoaPods 1.5.0 31 | 32 | ## Questions 33 | If you have questions about any aspect of this project, please feel free to contact me with the following email 34 |
Email: heyfiniks@gmail.com 35 |
or Weibo: @heyfiniks 36 |
37 | ## Screenshots 38 | ![](http://ww1.sinaimg.cn/large/0060lm7Tgw1f1dtb12v4gj30af0ijtaa.jpg) 39 | ![](http://ww1.sinaimg.cn/large/0060lm7Tgw1f1dtb1o68aj30af0ijmz0.jpg) 40 | ![](http://ww4.sinaimg.cn/large/0060lm7Tgw1f1dtb1yzxhj30af0ijtas.jpg) 41 | ![](http://ww4.sinaimg.cn/large/0060lm7Tgw1f0hmca4k9mj30af0ijtay.jpg) 42 | ![](http://ww3.sinaimg.cn/large/0060lm7Tgw1f0e4swtysvj30af0ijdgq.jpg) 43 | ![](http://ww3.sinaimg.cn/large/0060lm7Tgw1f0hmc9igxwj30af0ijta2.jpg) 44 | ![](http://ww1.sinaimg.cn/large/0060lm7Tgw1f2u1825fayj30af0ijq43.jpg) 45 | ![](http://ww2.sinaimg.cn/large/0060lm7Tgw1f0hmc9hn99j30af0ijjt8.jpg) 46 | ![](http://ww1.sinaimg.cn/large/0060lm7Tgw1f2u183dk5qj30af0ijgmy.jpg) 47 | ![](http://ww3.sinaimg.cn/large/0060lm7Tgw1f0e4sw8e04j30af0ijjs6.jpg) 48 | 49 |
50 | 51 | ## LICENSE 52 | 53 | [MIT](https://raw.githubusercontent.com/Finb/V2ex-Swift/master/LICENSE) © [Fin](http://github.com/Finb) 54 | -------------------------------------------------------------------------------- /View/V2FPSLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2FPSLabel.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/15/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | //重写自 YYFPSLabel 11 | //https://github.com/ibireme/YYText/blob/master/Demo/YYTextDemo/YYFPSLabel.m 12 | 13 | 14 | class V2FPSLabel: UILabel { 15 | fileprivate var _link :CADisplayLink? 16 | fileprivate var _count:Int = 0 17 | fileprivate var _lastTime:TimeInterval = 0 18 | 19 | fileprivate let _defaultSize = CGSize(width: 55, height: 20); 20 | 21 | override init(frame: CGRect) { 22 | var targetFrame = frame 23 | if frame.size.width == 0 && frame.size.height == 0{ 24 | targetFrame.size = _defaultSize 25 | } 26 | super.init(frame: targetFrame) 27 | self.layer.cornerRadius = 5 28 | self.clipsToBounds = true 29 | self.textAlignment = .center 30 | self.isUserInteractionEnabled = false 31 | self.textColor = UIColor.white 32 | self.backgroundColor = UIColor(white: 0, alpha: 0.7) 33 | self.font = UIFont(name: "Menlo", size: 14) 34 | weak var weakSelf = self 35 | _link = CADisplayLink(target: weakSelf!, selector:#selector(V2FPSLabel.tick(_:)) ); 36 | _link!.add(to: RunLoop.main, forMode:RunLoop.Mode.common) 37 | } 38 | required init?(coder aDecoder: NSCoder) { 39 | super.init(coder: aDecoder) 40 | } 41 | 42 | @objc func tick(_ link:CADisplayLink) { 43 | if _lastTime == 0 { 44 | _lastTime = link.timestamp 45 | return 46 | } 47 | 48 | _count += 1 49 | let delta = link.timestamp - _lastTime 50 | if delta < 1 { 51 | return 52 | } 53 | _lastTime = link.timestamp 54 | let fps = Double(_count) / delta 55 | _count = 0 56 | 57 | 58 | 59 | let progress = fps / 60.0; 60 | self.textColor = UIColor(hue: CGFloat(0.27 * ( progress - 0.2 )) , saturation: 1, brightness: 0.9, alpha: 1) 61 | self.text = "\(Int(fps+0.5))FPS" 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /V2ex-Swift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | 用于将图片保存在相册 7 | NSPhotoLibraryAddUsageDescription 8 | 用于将图片保存在相册 9 | CFBundleDevelopmentRegion 10 | en 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | V2EX 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(CURRENT_PROJECT_VERSION) 27 | Fabric 28 | 29 | APIKey 30 | e35a0878ffe50135a432ed19f88b19ba853f1a95 31 | Kits 32 | 33 | 34 | KitInfo 35 | 36 | KitName 37 | Crashlytics 38 | 39 | 40 | 41 | ITSAppUsesNonExemptEncryption 42 | 43 | LSApplicationQueriesSchemes 44 | 45 | org-appextension-feature-password-management 46 | 47 | LSRequiresIPhoneOS 48 | 49 | NSAppTransportSecurity 50 | 51 | NSAllowsArbitraryLoads 52 | 53 | 54 | UILaunchStoryboardName 55 | Launch Screen 56 | UIRequiredDeviceCapabilities 57 | 58 | armv7 59 | 60 | UIStatusBarStyle 61 | UIStatusBarStyleDefault 62 | UISupportedInterfaceOrientations 63 | 64 | UIInterfaceOrientationPortrait 65 | 66 | UIViewControllerBasedStatusBarAppearance 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /View/RightNodeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RightNodeTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/23/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RightNodeTableViewCell: UITableViewCell { 12 | 13 | var nodeNameLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = v2Font(15) 16 | return label 17 | }() 18 | 19 | var panel = UIView() 20 | 21 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 22 | super.init(style: style, reuseIdentifier: reuseIdentifier); 23 | 24 | self.setup(); 25 | } 26 | required init?(coder aDecoder: NSCoder) { 27 | super.init(coder: aDecoder) 28 | } 29 | func setup()->Void{ 30 | self.selectionStyle = .none 31 | self.backgroundColor = UIColor.clear 32 | 33 | self.contentView.addSubview(panel) 34 | self.panel.snp.makeConstraints{ (make) -> Void in 35 | make.left.top.right.equalTo(self.contentView) 36 | make.bottom.equalTo(self.contentView).offset(-1 * SEPARATOR_HEIGHT) 37 | } 38 | 39 | panel.addSubview(self.nodeNameLabel) 40 | self.nodeNameLabel.snp.makeConstraints{ (make) -> Void in 41 | make.right.equalTo(panel).offset(-22) 42 | make.centerY.equalTo(panel) 43 | } 44 | 45 | self.themeChangedHandler = {[weak self] (style) -> Void in 46 | self?.refreshBackgroundColor() 47 | self?.nodeNameLabel.textColor = V2EXColor.colors.v2_LeftNodeTintColor 48 | } 49 | } 50 | 51 | override func setSelected(_ selected: Bool, animated: Bool) { 52 | super.setSelected(selected, animated: animated); 53 | self.refreshBackgroundColor() 54 | } 55 | func refreshBackgroundColor() { 56 | if self.isSelected { 57 | self.panel.backgroundColor = V2EXColor.colors.v2_LeftNodeBackgroundHighLightedColor 58 | } 59 | else{ 60 | self.panel.backgroundColor = V2EXColor.colors.v2_LeftNodeBackgroundColor 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /View/V2PhotoBrowser/V2PhotoBrowserTransionPresent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2PhotoBrowserTransionPresent.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/26/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2PhotoBrowserTransionPresent:NSObject,UIViewControllerAnimatedTransitioning { 12 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 13 | return 0.3 14 | } 15 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 16 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! V2PhotoBrowser 17 | let container = transitionContext.containerView 18 | container.addSubview(toVC.view) 19 | 20 | //给引导动画赋值 21 | if let delegate = toVC.delegate{ 22 | toVC.guideImageView.frame = delegate.guideFrameInPhotoBrowser(toVC, index: toVC.currentPageIndex) 23 | toVC.guideImageView.image = delegate.guideImageInPhotoBrowser(toVC, index: toVC.currentPageIndex) 24 | toVC.guideImageView.contentMode = delegate.guideContentModeInPhotoBrowser(toVC, index: toVC.currentPageIndex) 25 | } 26 | 27 | //显示引导动画的imageView 28 | toVC.guideImageViewHidden(false) 29 | 30 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: UIView.AnimationOptions(), animations: { () -> Void in 31 | toVC.view.backgroundColor = UIColor(white: 0, alpha: 1) 32 | toVC.guideImageView.frame = toVC.view.bounds 33 | 34 | //如果图片过小,则直接中间原图显示 ,否则fit 35 | if let width = toVC.guideImageView.originalImage?.size.width, let height = toVC.guideImageView.originalImage?.size.height, width > SCREEN_WIDTH || height > SCREEN_HEIGHT { 36 | toVC.guideImageView.contentMode = .scaleAspectFit 37 | } 38 | else{ 39 | toVC.guideImageView.contentMode = .center 40 | } 41 | 42 | }) { (finished: Bool) -> Void in 43 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 44 | //隐藏引导动画 45 | toVC.guideImageViewHidden(true) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Common/Request+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/2/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | //import UIKit 10 | import Foundation 11 | import Alamofire 12 | import Ji 13 | extension Error { 14 | var localizedFailureReason :String? { 15 | return (self as NSError).localizedFailureReason 16 | } 17 | } 18 | extension DataRequest { 19 | enum ErrorCode: Int { 20 | case noData = 1 21 | case dataSerializationFailed = 2 22 | } 23 | internal static func newError(_ code: ErrorCode, failureReason: String) -> NSError { 24 | let errorDomain = "me.fin.v2ex.error" 25 | let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] 26 | let returnError = NSError(domain: errorDomain, code: code.rawValue, userInfo: userInfo) 27 | return returnError 28 | } 29 | 30 | static func JIHTMLResponseSerializer() -> DataResponseSerializer { 31 | return DataResponseSerializer { request, response, data, error in 32 | guard error == nil else { return .failure(error!) } 33 | 34 | if response?.url?.path == "/signin" && request?.url?.path != "/signin" { 35 | //跳转到登录页时,则证明请求的内容需要登录 36 | let failureReason = "查看的内容需要登录!" 37 | let error = newError(.dataSerializationFailed, failureReason: failureReason) 38 | return .failure(error) 39 | } 40 | guard let validData = data else { 41 | return .failure(AFError.responseSerializationFailed(reason: .inputDataNil)) 42 | } 43 | 44 | if let jiHtml = Ji(htmlData: validData){ 45 | return .success(jiHtml) 46 | } 47 | 48 | let failureReason = "ObjectMapper failed to serialize response." 49 | let error = newError(.dataSerializationFailed, failureReason: failureReason) 50 | return .failure(error) 51 | } 52 | } 53 | 54 | @discardableResult 55 | public func responseJiHtml(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { 56 | return response(responseSerializer: Alamofire.DataRequest.JIHTMLResponseSerializer(), completionHandler: completionHandler); 57 | } 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /Common/V2ex+Define.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2ex+Define.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/11/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | //屏幕宽度 12 | let SCREEN_WIDTH = UIScreen.main.bounds.size.width; 13 | //屏幕高度 14 | let SCREEN_HEIGHT = UIScreen.main.bounds.size.height; 15 | //NavagationBar高度 16 | let NavigationBarHeight:CGFloat = { 17 | return kSafeAreaInsets.top + 44 18 | }() 19 | let kSafeAreaInsets:UIEdgeInsets = { 20 | if #available(iOS 12.0, *){ 21 | return UIApplication.shared.keyWindow?.safeAreaInsets ?? UIWindow().safeAreaInsets 22 | } 23 | if UIDevice.current.isIphoneX { 24 | return UIEdgeInsets(top: 44, left: 0, bottom: 34, right: 0) 25 | } 26 | // iOS 11 下,普通机型的safeAreaInsets.top 是 0 ,与iOS12 的 20 不一致 27 | // 这里让他们的 safeAreaInsets.top 保持一致 28 | return UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) 29 | }() 30 | //用户代理,使用这个切换是获取 m站点 还是www站数据 31 | let USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Mobile/15E148 Safari/604.1"; 32 | let MOBILE_CLIENT_HEADERS = ["user-agent":USER_AGENT] 33 | 34 | 35 | //站点地址,客户端只有https,禁用http 36 | let V2EXURL = "https://www.v2ex.com/" 37 | 38 | let SEPARATOR_HEIGHT = 1.0 / UIScreen.main.scale 39 | 40 | 41 | func NSLocalizedString( _ key:String ) -> String { 42 | return NSLocalizedString(key, comment: "") 43 | } 44 | 45 | 46 | func dispatch_sync_safely_main_queue(_ block: ()->()) { 47 | if Thread.isMainThread { 48 | block() 49 | } else { 50 | DispatchQueue.main.sync { 51 | block() 52 | } 53 | } 54 | } 55 | 56 | func v2Font(_ fontSize: CGFloat) -> UIFont { 57 | return UIFont.systemFont(ofSize: fontSize); 58 | } 59 | 60 | func v2ScaleFont(_ fontSize: CGFloat) -> UIFont{ 61 | return v2Font(fontSize * CGFloat(V2Style.sharedInstance.fontScale)) 62 | } 63 | 64 | 65 | extension UIDevice { 66 | var isIphoneX: Bool { 67 | get { 68 | // 一般 top 为 44, iPhone 11 的为 48 69 | return kSafeAreaInsets.top >= 44 70 | } 71 | } 72 | } 73 | 74 | extension UITableView { 75 | func cancelEstimatedHeight(){ 76 | self.estimatedRowHeight = 120 77 | self.rowHeight = UITableView.automaticDimension // Self-sizing cell 78 | self.estimatedSectionFooterHeight = 0 79 | self.estimatedSectionHeaderHeight = 0 80 | 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /Controller/AgreementViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AgreementViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2020/9/13. 6 | // Copyright © 2020 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AgreementViewController: UIViewController { 12 | let textLabel:UILabel = { 13 | let label = UILabel() 14 | let text = """ 15 | V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。 16 | 17 | 希望大家能够多多分享自己正在做的有趣事物、交流想法,在这里找到朋友甚至新的机会。并且,最重要的是,在这一切的过程中,保持对他人的友善。 18 | 19 | 为了保持这里的良好氛围,V2EX 有自己的明确规则: 20 | 21 | • 这里绝对不讨论任何有关盗版软件、音乐、电影如何获得的问题 22 | • 这里绝对不会全文转载任何文章,而只会以链接方式分享1 23 | • 这里绝对不会有任何教人如何钻空子的讨论 24 | • 这里感激和崇尚美的事物 25 | • 这里尊重原创 26 | • 这里反对中文互联网上的无信息量习惯如“顶”,“沙发”,“前排”,“留名”,“路过”,“不明觉厉”2 27 | • 这里禁止发布人身攻击、仇恨、暴力、侮辱性的言辞、暴露他人隐私的“人肉贴” 28 | • 当你在网上发帖时,请考虑到你所做的一切,会受到你所在地区法律的管辖 29 | • V2EX 不反对文章的原作者自己全文转载自己写的原创文章 30 | • “路过”,“沙发”之类的 0 信息量回复会被自动规则阻挡或者被管理员删除。和讨论主题完全无关的回复,尤其是在技术类讨论主题下出现的话,会被管理员删除。 31 | """ 32 | let style = NSMutableParagraphStyle() 33 | style.lineSpacing = 5 34 | let attributedText = NSMutableAttributedString(string: text, attributes: [ 35 | .foregroundColor : V2EXColor.colors.v2_TopicListTitleColor, 36 | .font : v2Font(15), 37 | .paragraphStyle: style]) 38 | label.attributedText = attributedText 39 | label.numberOfLines = 0 40 | return label 41 | }() 42 | let scrollView:UIScrollView = { 43 | let scrollView = UIScrollView() 44 | return scrollView 45 | }() 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | self.title = NSLocalizedString("userAgreement") 49 | 50 | self.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 51 | 52 | self.view.addSubview(scrollView) 53 | scrollView.snp.makeConstraints { (make) in 54 | make.edges.equalToSuperview() 55 | } 56 | 57 | scrollView.addSubview(textLabel) 58 | textLabel.snp.makeConstraints { (make) in 59 | make.top.left.equalToSuperview().offset(15) 60 | make.bottom.equalToSuperview().offset(-15) 61 | make.width.equalTo(SCREEN_WIDTH - 20) 62 | } 63 | } 64 | override func viewDidLayoutSubviews() { 65 | super.viewDidLayoutSubviews() 66 | // self.scrollView.contentSize = CGSize(width: SCREEN_WIDTH, height: 20 + textLabel.bounds.size.height) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Model/NodeModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeModel.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/2/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Alamofire 11 | import Ji 12 | 13 | class NodeModel: NSObject ,BaseHtmlModelProtocol{ 14 | var nodeId:String? 15 | var nodeName:String? 16 | var width:CGFloat = 0 17 | override init() { 18 | super.init() 19 | } 20 | required init(rootNode: JiNode) { 21 | self.nodeName = rootNode.content 22 | if let nodeName = self.nodeName { 23 | //计算字符串所占的宽度 24 | //用于之后这个 node 在 cell 中所占的宽度 25 | let rect = (nodeName as NSString).boundingRect( 26 | with: CGSize(width: SCREEN_WIDTH,height: 15), 27 | options: .usesLineFragmentOrigin, 28 | attributes: [NSAttributedString.Key.font:v2Font(15)], context: nil) 29 | 30 | self.width = rect.width; 31 | } 32 | 33 | if var href = rootNode["href"] { 34 | if let range = href.range(of: "/go/") { 35 | href.replaceSubrange(range, with: ""); 36 | self.nodeId = href 37 | } 38 | } 39 | } 40 | } 41 | class NodeGroupModel: NSObject ,BaseHtmlModelProtocol{ 42 | var groupName:String? 43 | var childrenRows:[[Int]] = [[]] 44 | var children:[NodeModel] = [] 45 | required init(rootNode: JiNode) { 46 | self.groupName = rootNode.xPath("./td[1]/span").first?.content 47 | for node in rootNode.xPath("./td[2]/a") { 48 | self.children.append(NodeModel(rootNode: node)) 49 | } 50 | } 51 | 52 | class func getNodes( _ completionHandler: ((V2ValueResponse<[NodeGroupModel]>) -> Void)? = nil ) { 53 | Alamofire.request(V2EXURL, headers: MOBILE_CLIENT_HEADERS).responseJiHtml { (response) in 54 | var groupArray : [NodeGroupModel] = [] 55 | if let jiHtml = response .result.value{ 56 | if let nodes = jiHtml.xPath("//*[@id='Wrapper']/div/div[@class='box'][last()]/div/table/tr") { 57 | for rootNode in nodes { 58 | let group = NodeGroupModel(rootNode: rootNode) 59 | groupArray.append(group) 60 | } 61 | } 62 | completionHandler?(V2ValueResponse(value: groupArray, success: true)) 63 | return; 64 | } 65 | completionHandler?(V2ValueResponse(success: false, message: "获取失败")) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /View/TopicDetailToolCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopicDetailToolCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2018/12/6. 6 | // Copyright © 2018 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TopicDetailToolCell: UITableViewCell { 12 | var titleLabel:UILabel = { 13 | let label = UILabel() 14 | label.font = v2Font(12) 15 | return label 16 | }() 17 | 18 | var separator:UIImageView = UIImageView() 19 | let sortButton:V2HitTestSlopButton = { 20 | let btn = V2HitTestSlopButton() 21 | btn.titleLabel?.font = v2Font(12) 22 | btn.hitTestSlop = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) 23 | btn.titleLabel?.textAlignment = .left 24 | return btn 25 | }() 26 | var sortButtonClick:((_ sender:UIButton) -> Void)? 27 | 28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 29 | super.init(style: style, reuseIdentifier: reuseIdentifier); 30 | self.setup(); 31 | } 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | } 35 | 36 | func setup() { 37 | self.selectionStyle = .none 38 | 39 | self.contentView.addSubview(sortButton) 40 | self.contentView.addSubview(self.titleLabel) 41 | self.contentView.addSubview(self.separator) 42 | 43 | sortButton.snp.makeConstraints { (make) in 44 | make.centerY.equalToSuperview() 45 | make.left.equalToSuperview().offset(12) 46 | } 47 | 48 | self.titleLabel.snp.makeConstraints{ (make) -> Void in 49 | make.left.equalTo(self.sortButton.snp.right).offset(8) 50 | make.centerY.equalTo(self.contentView) 51 | } 52 | self.separator.snp.makeConstraints{ (make) -> Void in 53 | make.left.right.bottom.equalTo(self.contentView) 54 | make.height.equalTo(SEPARATOR_HEIGHT) 55 | } 56 | 57 | self.themeChangedHandler = {[weak self] (style) -> Void in 58 | self?.sortButton.setTitleColor(V2EXColor.colors.v2_TopicListTitleColor, for: .normal) 59 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 60 | self?.titleLabel.textColor = V2EXColor.colors.v2_TopicListTitleColor 61 | self?.separator.image = createImageWithColor(V2EXColor.colors.v2_backgroundColor) 62 | } 63 | 64 | self.sortButton.addTarget(self, action: #selector(sortClick(sender:)), for: .touchUpInside) 65 | } 66 | 67 | @objc func sortClick(sender:UIButton){ 68 | sortButtonClick?(sender) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Model/NotificationsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsModel.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/29/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import Alamofire 12 | import Ji 13 | 14 | class NotificationsModel: NSObject,BaseHtmlModelProtocol { 15 | var avata: String? 16 | var userName: String? 17 | var title: String? 18 | var date: String? 19 | var reply: String? 20 | 21 | var topicId: String? 22 | 23 | required init(rootNode: JiNode) { 24 | self.avata = rootNode.xPath("./table/tr/td[1]/a/img[@class='avatar']").first?["src"] 25 | self.userName = rootNode.xPath("./table/tr/td[2]/span[1]/a[1]/strong").first?.content 26 | self.title = rootNode.xPath("./table/tr/td[2]/span[1]").first?.content 27 | self.date = rootNode.xPath("./table/tr/td[2]/span[2]").first?.content 28 | self.reply = rootNode.xPath("./table/tr/td[2]/div[@class='payload']").first?.content 29 | 30 | if let node = rootNode.xPath("./table/tr/td[2]/span[1]/a[2]").first { 31 | var topicIdUrl = node["href"]; 32 | 33 | if var id = topicIdUrl { 34 | if let range = id.range(of: "/t/") { 35 | id.replaceSubrange(range, with: ""); 36 | } 37 | if let range = id.range(of: "#") { 38 | topicIdUrl = String(id[..) -> Void)? = nil){ 51 | 52 | Alamofire.request(V2EXURL+"notifications", headers: MOBILE_CLIENT_HEADERS).responseJiHtml { (response) in 53 | var resultArray:[NotificationsModel] = [] 54 | 55 | if let jiHtml = response.result.value { 56 | if let aRootNode = jiHtml.xPath("//*[@id=\"notifications\"]/div[attribute::id]"){ 57 | for aNode in aRootNode { 58 | let notice = NotificationsModel(rootNode:aNode) 59 | resultArray.append(notice); 60 | } 61 | 62 | //更新通知数量 63 | V2User.sharedInstance.getNotificationsCount(jiHtml.rootNode!) 64 | } 65 | } 66 | 67 | let t = V2ValueResponse<[NotificationsModel]>(value:resultArray, success: response.result.isSuccess) 68 | completionHandler?(t); 69 | 70 | } 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /View/V2LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2LoadingView.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/28/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let noticeString = [ 12 | "正在拼命加载", 13 | "前方发现楼主", 14 | "年轻人,不要着急", 15 | "让我飞一会儿", 16 | "大爷,您又来了?", 17 | "楼主正在抓皮卡丘,等他一会儿吧", 18 | "爱我,就等我一万年", 19 | "未满18禁止入内", 20 | "正在前往 花村", 21 | "正在前往 阿努比斯神殿", 22 | "正在前往 沃斯卡娅工业区", 23 | "正在前往 观测站:直布罗陀", 24 | "正在前往 好莱坞", 25 | "正在前往 66号公路", 26 | "正在前往 国王大道", 27 | "正在前往 伊利奥斯", 28 | "正在前往 漓江塔", 29 | "正在前往 尼泊尔" 30 | ] 31 | 32 | class V2LoadingView: UIView { 33 | var activityIndicatorView = UIActivityIndicatorView(style: .gray) 34 | init (){ 35 | super.init(frame:CGRect.zero) 36 | self.addSubview(self.activityIndicatorView) 37 | self.activityIndicatorView.snp.makeConstraints{ (make) -> Void in 38 | make.centerX.equalTo(self) 39 | make.centerY.equalTo(self).offset(-32) 40 | } 41 | 42 | let noticeLabel = UILabel() 43 | //修复BUG。做个小笔记给阅读代码的兄弟们提个醒 44 | //(Int)(arc4random()) 45 | //上面这种写法有问题,arc4random()会返回 一个Uint32的随机数值。 46 | //在32位机器上,如果随机的数大于Int.max ,转换就会crash。 47 | noticeLabel.text = noticeString[Int(arc4random() % UInt32(noticeString.count))] 48 | noticeLabel.font = v2Font(10) 49 | noticeLabel.textColor = V2EXColor.colors.v2_TopicListDateColor 50 | self.addSubview(noticeLabel) 51 | noticeLabel.snp.makeConstraints{ (make) -> Void in 52 | make.top.equalTo(self.activityIndicatorView.snp.bottom).offset(10) 53 | make.centerX.equalTo(self.activityIndicatorView) 54 | } 55 | 56 | self.themeChangedHandler = {[weak self] (style) -> Void in 57 | if V2EXColor.sharedInstance.style == V2EXColor.V2EXColorStyleDefault { 58 | self?.activityIndicatorView.style = .gray 59 | } 60 | else{ 61 | self?.activityIndicatorView.style = .white 62 | } 63 | } 64 | } 65 | 66 | override func willMove(toSuperview newSuperview: UIView?) { 67 | self.activityIndicatorView.startAnimating() 68 | } 69 | 70 | required init?(coder aDecoder: NSCoder) { 71 | fatalError("init(coder:) has not been implemented") 72 | } 73 | 74 | func hide(){ 75 | self.superview?.bringSubviewToFront(self) 76 | 77 | UIView.animate(withDuration: 0.2, 78 | animations: { () -> Void in 79 | self.alpha = 0 ; 80 | }, completion: { (finished) -> Void in 81 | if finished { 82 | self.removeFromSuperview(); 83 | } 84 | }) 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /View/V2PhotoBrowser/V2PhotoBrowserSwipeInteractiveTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2PhotoBrowserSwipeInteractiveTransition.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/26/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit.UIGestureRecognizerSubclass 10 | import CXSwipeGestureRecognizer 11 | 12 | class V2PhotoBrowserSwipeInteractiveTransition: UIPercentDrivenInteractiveTransition ,CXSwipeGestureRecognizerDelegate { 13 | weak var browser:V2PhotoBrowser? 14 | 15 | var interacting:Bool = false 16 | fileprivate var dismissing = false 17 | 18 | var shouldComplete:Bool = false 19 | 20 | var direction:CXSwipeGestureDirection = CXSwipeGestureDirection() 21 | 22 | var gestureRecognizer = CXSwipeGestureRecognizer() 23 | 24 | func prepareGestureRecognizerInView(_ view:UIView){ 25 | 26 | gestureRecognizer.view?.removeGestureRecognizer(gestureRecognizer) 27 | 28 | gestureRecognizer.delegate = self 29 | view.addGestureRecognizer(gestureRecognizer) 30 | } 31 | func swipeGestureRecognizerDidStart(_ gestureRecognizer: CXSwipeGestureRecognizer!){ 32 | self.interacting = true 33 | } 34 | func swipeGestureRecognizerDidUpdate(_ gestureRecognizer: CXSwipeGestureRecognizer!){ 35 | 36 | if (gestureRecognizer.currentDirection() != .downwards && gestureRecognizer.currentDirection() != .upwards) || !self.interacting{ 37 | gestureRecognizer.state = .cancelled 38 | self.cancelEvent() 39 | return 40 | } 41 | 42 | 43 | if !self.dismissing { 44 | self.dismissing = true 45 | self.browser?.dismiss(animated: true, completion: nil) 46 | } 47 | 48 | 49 | self.direction = gestureRecognizer.currentDirection() 50 | 51 | var fraction = Float(gestureRecognizer.translation(in: gestureRecognizer.currentDirection()) / self.browser!.view.bounds.size.height) 52 | fraction = fminf(fmaxf(fraction, 0.0), 1.0) 53 | self.shouldComplete = abs(fraction) > 0.3 54 | self.update(CGFloat(abs(fraction))) 55 | } 56 | func swipeGestureRecognizerDidFinish(_ gestureRecognizer: CXSwipeGestureRecognizer!){ 57 | self.dismissing = false 58 | self.interacting = false 59 | if self.shouldComplete || gestureRecognizer.velocity(in: gestureRecognizer.currentDirection()) > 600{ 60 | self.finish() 61 | } 62 | else{ 63 | self.cancelEvent() 64 | } 65 | 66 | } 67 | 68 | func cancelEvent(){ 69 | self.dismissing = false 70 | self.interacting = false 71 | self.direction = CXSwipeGestureDirection() 72 | self.cancel() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Common/V2LeftAlignedCollectionViewFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2LeftAlignedCollectionViewFlowLayout.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 16/4/5. 6 | // Copyright © 2016年 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { 12 | var cellSpacing:CGFloat = 15 13 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 14 | let attributesToReturn = super.layoutAttributesForElements(in: rect); 15 | guard attributesToReturn != nil else{ 16 | return attributesToReturn; 17 | } 18 | 19 | for attributes in attributesToReturn! { 20 | if attributes.representedElementKind == nil { 21 | let indexPath = attributes.indexPath; 22 | attributes.frame = self.layoutAttributesForItem(at: indexPath).frame; 23 | } 24 | } 25 | return attributesToReturn; 26 | } 27 | 28 | override func layoutAttributesForItem(at indexPath:IndexPath) -> UICollectionViewLayoutAttributes { 29 | let currentItemAttributes = super.layoutAttributesForItem(at: indexPath) 30 | 31 | let sectionInset = self.sectionInset 32 | 33 | if indexPath.item == 0 { 34 | var frame = currentItemAttributes!.frame 35 | frame.origin.x = sectionInset.left 36 | currentItemAttributes!.frame = frame 37 | return currentItemAttributes!; 38 | } 39 | 40 | let previousIndexPath = IndexPath(item: indexPath.item - 1 , section: indexPath.section); 41 | let previousFrame = self.layoutAttributesForItem(at: previousIndexPath).frame 42 | let previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + cellSpacing; 43 | let currentFrame = currentItemAttributes?.frame; 44 | let strecthedCurrentFrame = CGRect(x: 0, 45 | y: currentFrame!.origin.y, 46 | width: self.collectionView!.frame.size.width, 47 | height: currentFrame!.size.height); 48 | if !previousFrame.intersects(strecthedCurrentFrame) { 49 | var frame = currentItemAttributes!.frame; 50 | frame.origin.x = sectionInset.left; // first item on the line should always be left aligned 51 | currentItemAttributes!.frame = frame; 52 | return currentItemAttributes!; 53 | } 54 | 55 | var frame = currentItemAttributes!.frame; 56 | frame.origin.x = previousFrameRightPoint; 57 | currentItemAttributes!.frame = frame; 58 | return currentItemAttributes!; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /View/V2RefreshHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2RefreshHeader.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/27/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MJRefresh 11 | 12 | class V2RefreshHeader: MJRefreshHeader { 13 | var loadingView:UIActivityIndicatorView? 14 | var arrowImage:UIImageView? 15 | 16 | override var state:MJRefreshState{ 17 | didSet{ 18 | switch state { 19 | case .idle: 20 | self.loadingView?.isHidden = true 21 | self.arrowImage?.isHidden = false 22 | self.loadingView?.stopAnimating() 23 | case .pulling: 24 | self.loadingView?.isHidden = false 25 | self.arrowImage?.isHidden = true 26 | self.loadingView?.startAnimating() 27 | 28 | case .refreshing: 29 | self.loadingView?.isHidden = false 30 | self.arrowImage?.isHidden = true 31 | self.loadingView?.startAnimating() 32 | default: 33 | NSLog("") 34 | } 35 | } 36 | } 37 | 38 | /** 39 | 初始化工作 40 | */ 41 | override func prepare() { 42 | super.prepare() 43 | self.mj_h = 50 44 | 45 | self.loadingView = UIActivityIndicatorView(style: .white) 46 | self.addSubview(self.loadingView!) 47 | 48 | self.arrowImage = UIImageView(image: UIImage.imageUsedTemplateMode("ic_arrow_downward")) 49 | self.addSubview(self.arrowImage!) 50 | 51 | self.themeChangedHandler = {[weak self] (style) -> Void in 52 | if V2EXColor.sharedInstance.style == V2EXColor.V2EXColorStyleDefault { 53 | self?.loadingView?.style = .gray 54 | self?.arrowImage?.tintColor = UIColor.gray 55 | } 56 | else{ 57 | self?.loadingView?.style = .white 58 | self?.arrowImage?.tintColor = UIColor.gray 59 | } 60 | } 61 | } 62 | 63 | /** 64 | 在这里设置子控件的位置和尺寸 65 | */ 66 | override func placeSubviews(){ 67 | super.placeSubviews() 68 | self.loadingView!.center = CGPoint(x: self.mj_w/2, y: self.mj_h/2); 69 | self.arrowImage!.frame = CGRect(x: 0, y: 0, width: 24, height: 24) 70 | self.arrowImage!.center = self.loadingView!.center 71 | } 72 | 73 | override func scrollViewContentOffsetDidChange(_ change: [AnyHashable: Any]!) { 74 | super.scrollViewContentOffsetDidChange(change) 75 | } 76 | 77 | override func scrollViewContentSizeDidChange(_ change: [AnyHashable: Any]!) { 78 | super.scrollViewContentOffsetDidChange(change) 79 | } 80 | 81 | override func scrollViewPanStateDidChange(_ change: [AnyHashable: Any]!) { 82 | super.scrollViewPanStateDidChange(change) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /View/V2RefreshFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2RefreshFooter.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/1/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MJRefresh 11 | 12 | class V2RefreshFooter: MJRefreshAutoFooter { 13 | 14 | var loadingView:UIActivityIndicatorView? 15 | var stateLabel:UILabel? 16 | 17 | var centerOffset:CGFloat = 0 18 | 19 | fileprivate var _noMoreDataStateString:String? 20 | var noMoreDataStateString:String? { 21 | get{ 22 | return self._noMoreDataStateString 23 | } 24 | set{ 25 | self._noMoreDataStateString = newValue 26 | self.stateLabel?.text = newValue 27 | } 28 | } 29 | 30 | override var state:MJRefreshState{ 31 | didSet{ 32 | switch state { 33 | case .idle: 34 | self.stateLabel?.text = nil 35 | self.loadingView?.isHidden = true 36 | self.loadingView?.stopAnimating() 37 | case .refreshing: 38 | self.stateLabel?.text = nil 39 | self.loadingView?.isHidden = false 40 | self.loadingView?.startAnimating() 41 | case .noMoreData: 42 | self.stateLabel?.text = self.noMoreDataStateString 43 | self.loadingView?.isHidden = true 44 | self.loadingView?.stopAnimating() 45 | default:break 46 | } 47 | } 48 | } 49 | 50 | /** 51 | 初始化工作 52 | */ 53 | override func prepare() { 54 | super.prepare() 55 | self.mj_h = 50 56 | 57 | self.loadingView = UIActivityIndicatorView(style: .white) 58 | self.addSubview(self.loadingView!) 59 | 60 | self.stateLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 40)) 61 | self.stateLabel?.textAlignment = .center 62 | self.stateLabel!.font = v2Font(12) 63 | self.addSubview(self.stateLabel!) 64 | 65 | self.noMoreDataStateString = "没有更多数据了" 66 | 67 | self.themeChangedHandler = {[weak self] (style) -> Void in 68 | if V2EXColor.sharedInstance.style == V2EXColor.V2EXColorStyleDefault { 69 | self?.loadingView?.style = .gray 70 | self?.stateLabel!.textColor = UIColor(white: 0, alpha: 0.3) 71 | } 72 | else{ 73 | self?.loadingView?.style = .white 74 | self?.stateLabel!.textColor = UIColor(white: 1, alpha: 0.3) 75 | } 76 | } 77 | } 78 | 79 | /** 80 | 在这里设置子控件的位置和尺寸 81 | */ 82 | override func placeSubviews(){ 83 | super.placeSubviews() 84 | self.loadingView!.center = CGPoint(x: self.mj_w/2, y: self.mj_h/2 + self.centerOffset); 85 | self.stateLabel!.center = CGPoint(x: self.mj_w/2, y: self.mj_h/2 + self.centerOffset); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /View/V2PhotoBrowser/V2Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Photo.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/22/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | class V2Photo :NSObject{ 13 | static let V2PHOTO_PROGRESS_NOTIFICATION = "ME.FIN.V2PHOTO_PROGRESS_NOTIFICATION" 14 | static let V2PHOTO_LOADING_DID_END_NOTIFICATION = "ME.FIN.V2PHOTO_LOADING_DID_END_NOTIFICATION" 15 | 16 | var underlyingImage:UIImage? 17 | 18 | var url:URL 19 | 20 | init(url:URL) { 21 | 22 | if let scheme = url.scheme?.lowercased() , !["https","http"].contains(scheme){ 23 | assert(true, "url.scheme must be a HTTP/HTTPS request") 24 | } 25 | self.url = url 26 | } 27 | 28 | func performLoadUnderlyingImageAndNotify(){ 29 | if self.underlyingImage != nil{ 30 | return ; 31 | } 32 | 33 | let resource = KF.ImageResource(downloadURL: self.url) 34 | KingfisherManager.shared.cache.retrieveImage(forKey: resource.cacheKey, options: nil) { result -> () in 35 | let image = try? result.get().image 36 | 37 | if image != nil { 38 | dispatch_sync_safely_main_queue({ () -> () in 39 | self.imageLoadingComplete(image) 40 | }) 41 | } 42 | else{ 43 | KingfisherManager.shared.downloader.downloadImage(with: resource.downloadURL, options: nil, progressBlock: { (receivedSize, totalSize) -> () in 44 | let progress = Float(receivedSize) / Float(totalSize) 45 | let dict = [ 46 | "progress":progress, 47 | "photo":self 48 | ] as [String : Any] 49 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: V2Photo.V2PHOTO_PROGRESS_NOTIFICATION), object: dict) 50 | }){ result -> () in 51 | let image = try? result.get().image 52 | let originalData = try? result.get().originalData 53 | dispatch_sync_safely_main_queue({ () -> () in 54 | self.imageLoadingComplete(image) 55 | }) 56 | 57 | if let image = image { 58 | //保存图片缓存 59 | KingfisherManager.shared.cache.store(image, original: originalData, forKey: resource.cacheKey, toDisk: true, completionHandler: nil) 60 | } 61 | } 62 | 63 | 64 | } 65 | } 66 | } 67 | 68 | func imageLoadingComplete(_ image:UIImage?){ 69 | self.underlyingImage = image 70 | NotificationCenter.default.post(name: Notification.Name(rawValue: V2Photo.V2PHOTO_LOADING_DID_END_NOTIFICATION), object: self) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /View/LeftUserHeadCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftUserHeadCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/23/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import KVOController 11 | import Kingfisher 12 | 13 | class LeftUserHeadCell: UITableViewCell { 14 | /// 头像 15 | var avatarImageView: UIImageView = { 16 | let imageView = UIImageView() 17 | imageView.backgroundColor = UIColor(white: 0.9, alpha: 0.3) 18 | imageView.layer.borderWidth = 1.5 19 | imageView.layer.borderColor = UIColor(white: 1, alpha: 0.6).cgColor 20 | imageView.layer.masksToBounds = true 21 | imageView.layer.cornerRadius = 38 22 | return imageView 23 | }() 24 | /// 用户名 25 | var userNameLabel: UILabel = { 26 | let label = UILabel() 27 | label.font = v2Font(16) 28 | return label 29 | }() 30 | 31 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 32 | super.init(style: style, reuseIdentifier: reuseIdentifier); 33 | self.setup(); 34 | } 35 | required init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | } 38 | func setup()->Void{ 39 | self.backgroundColor = UIColor.clear 40 | self.selectionStyle = .none 41 | 42 | self.contentView.addSubview(self.avatarImageView) 43 | self.contentView.addSubview(self.userNameLabel) 44 | 45 | self.avatarImageView.snp.makeConstraints{ (make) -> Void in 46 | make.centerX.equalTo(self.contentView) 47 | make.centerY.equalTo(self.contentView).offset(-8) 48 | make.width.height.equalTo(self.avatarImageView.layer.cornerRadius * 2) 49 | } 50 | self.userNameLabel.snp.makeConstraints{ (make) -> Void in 51 | make.top.equalTo(self.avatarImageView.snp.bottom).offset(10) 52 | make.centerX.equalTo(self.avatarImageView) 53 | } 54 | 55 | self.kvoController.observe(V2User.sharedInstance, keyPath: "username", options: [.initial , .new]){ 56 | [weak self] (observe, observer, change) -> Void in 57 | if let weakSelf = self { 58 | weakSelf.userNameLabel.text = V2User.sharedInstance.username ?? "请先登录" 59 | if let avatar = V2User.sharedInstance.user?.avatar_large?.avatarString { 60 | weakSelf.avatarImageView.kf.setImage(with: URL(string: avatar)!, placeholder: nil, options: nil, completionHandler: { (result) -> () in 61 | //如果请求到图片时,客户端已经不是登录状态了,则将图片清除 62 | if !V2User.sharedInstance.isLogin { 63 | weakSelf.avatarImageView.image = nil 64 | } 65 | }) 66 | } 67 | else { //没有登录 68 | weakSelf.avatarImageView.image = nil 69 | } 70 | } 71 | } 72 | 73 | self.themeChangedHandler = {[weak self] (style) -> Void in 74 | self?.userNameLabel.textColor = V2EXColor.colors.v2_TopicListUserNameColor 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Common/UIImageView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extension.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/3/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | private var lastURLKey: Void? 13 | 14 | extension UIImageView { 15 | 16 | public var fin_webURL: URL? { 17 | return objc_getAssociatedObject(self, &lastURLKey) as? URL 18 | } 19 | 20 | fileprivate func fin_setWebURL(_ URL: Foundation.URL) { 21 | objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 22 | } 23 | 24 | func fin_setImageWithUrl (_ URL: Foundation.URL ,placeholderImage: UIImage? = nil 25 | ,imageModificationClosure:((_ image:UIImage) -> UIImage)? = nil){ 26 | 27 | self.image = placeholderImage 28 | 29 | let resource = KF.ImageResource(downloadURL: URL) 30 | fin_setWebURL(resource.downloadURL) 31 | KingfisherManager.shared.cache.retrieveImage(forKey: resource.cacheKey, options: nil) { result -> () in 32 | let image = try? result.get().image 33 | if image != nil { 34 | dispatch_sync_safely_main_queue({ () -> () in 35 | self.image = image 36 | }) 37 | } 38 | else { 39 | KingfisherManager.shared.downloader.downloadImage(with: resource.downloadURL, options: nil, progressBlock: nil, completionHandler: { (result) -> () in 40 | 41 | switch result { 42 | case .success(let imageResult): 43 | let originalData = imageResult.originalData 44 | let imageURL = imageResult.url 45 | var image = imageResult.image 46 | //处理图片 47 | if let img = imageModificationClosure?(image) { 48 | image = img 49 | } 50 | 51 | //保存图片缓存 52 | KingfisherManager.shared.cache.store(image, original: originalData, forKey: resource.cacheKey, toDisk: true, completionHandler: nil) 53 | self.fin_setImage(image, imageURL: imageURL!) 54 | case .failure: 55 | break 56 | } 57 | 58 | }) 59 | } 60 | } 61 | } 62 | 63 | fileprivate func fin_setImage(_ image:UIImage,imageURL:URL) { 64 | 65 | dispatch_sync_safely_main_queue { () -> () in 66 | guard imageURL == self.fin_webURL else { 67 | return 68 | } 69 | self.image = image 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | func fin_defaultImageModification() -> ((_ image:UIImage) -> UIImage) { 77 | return { ( image) -> UIImage in 78 | let roundedImage = image.roundedCornerImageWithCornerRadius(3) 79 | return roundedImage 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Common/V2UsersKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Keychain.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/11/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import KeychainSwift 11 | 12 | class V2UsersKeychain { 13 | static let sharedInstance = V2UsersKeychain() 14 | fileprivate let keychain = KeychainSwift() 15 | 16 | fileprivate(set) var users:[String:LocalSecurityAccountModel] = [:] 17 | 18 | fileprivate init() { 19 | let _ = loadUsersDict() 20 | } 21 | 22 | func addUser(_ user:LocalSecurityAccountModel){ 23 | if let username = user.username{ 24 | self.users[username] = user 25 | self.saveUsersDict() 26 | } 27 | else { 28 | assert(false, "username must not be 'nil'") 29 | } 30 | } 31 | func addUser(_ username:String,password:String,avata:String? = nil) { 32 | let user = LocalSecurityAccountModel() 33 | user.username = username 34 | user.avatar = avata 35 | self.addUser(user) 36 | } 37 | 38 | static let usersKey = "me.fin.testDict" 39 | func saveUsersDict(){ 40 | let data = NSMutableData() 41 | let archiver = NSKeyedArchiver(forWritingWith: data) 42 | archiver.encode(self.users) 43 | archiver.finishEncoding() 44 | keychain.set(data as Data, forKey: V2UsersKeychain.usersKey); 45 | } 46 | func loadUsersDict() -> [String:LocalSecurityAccountModel]{ 47 | if users.count <= 0 { 48 | let data = keychain.getData(V2UsersKeychain.usersKey) 49 | if let data = data{ 50 | let archiver = NSKeyedUnarchiver(forReadingWith: data) 51 | let usersDict = archiver.decodeObject() 52 | archiver.finishDecoding() 53 | if let usersDict = usersDict as? [String : LocalSecurityAccountModel] { 54 | self.users = usersDict 55 | } 56 | } 57 | } 58 | return self.users 59 | } 60 | 61 | func removeUser(_ username:String){ 62 | self.users.removeValue(forKey: username) 63 | self.saveUsersDict() 64 | } 65 | func removeAll(){ 66 | self.users = [:] 67 | self.saveUsersDict() 68 | } 69 | 70 | func update(_ username:String,password:String? = nil,avatar:String? = nil){ 71 | if let user = self.users[username] { 72 | if let avatar = avatar { 73 | user.avatar = avatar 74 | } 75 | self.saveUsersDict() 76 | } 77 | } 78 | 79 | } 80 | 81 | 82 | /// 将会序列化后保存进keychain中的 账户model 83 | class LocalSecurityAccountModel :NSObject, NSCoding { 84 | var username:String? 85 | var avatar:String? 86 | override init(){ 87 | 88 | } 89 | required init?(coder aDecoder: NSCoder){ 90 | self.username = aDecoder.decodeObject(forKey: "username") as? String 91 | self.avatar = aDecoder.decodeObject(forKey: "avatar") as? String 92 | } 93 | func encode(with aCoder: NSCoder){ 94 | aCoder.encode(self.username, forKey: "username") 95 | aCoder.encode(self.avatar, forKey: "avatar") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /View/FontSizeSliderTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontSizeSliderTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/10/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FontSizeSliderTableViewCell: UITableViewCell { 12 | 13 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 14 | super.init(style: style, reuseIdentifier: reuseIdentifier); 15 | self.setup(); 16 | } 17 | required init?(coder aDecoder: NSCoder) { 18 | super.init(coder: aDecoder) 19 | } 20 | func setup() { 21 | self.selectionStyle = .none 22 | 23 | let leftLabel = UILabel() 24 | leftLabel.font = v2Font(14 * 0.8) 25 | leftLabel.text = "A" 26 | leftLabel.textAlignment = .center 27 | self.contentView.addSubview(leftLabel) 28 | leftLabel.snp.makeConstraints{ (make) -> Void in 29 | make.centerY.equalTo(self.contentView) 30 | make.width.height.equalTo(30) 31 | make.left.equalTo(self.contentView) 32 | } 33 | 34 | let rightLabel = UILabel() 35 | rightLabel.font = v2Font(14 * 1.6) 36 | rightLabel.text = "A" 37 | rightLabel.textAlignment = .center 38 | self.contentView.addSubview(rightLabel) 39 | rightLabel.snp.makeConstraints{ (make) -> Void in 40 | make.centerY.equalTo(self.contentView) 41 | make.width.height.equalTo(30) 42 | make.right.equalTo(self.contentView) 43 | } 44 | 45 | let slider = V2Slider() 46 | slider.valueChanged = { (fontSize) in 47 | let size = fontSize * 0.05 + 0.8 48 | if V2Style.sharedInstance.fontScale != size { 49 | V2Style.sharedInstance.fontScale = size 50 | } 51 | } 52 | self.contentView.addSubview(slider) 53 | slider.snp.makeConstraints{ (make) -> Void in 54 | make.left.equalTo(leftLabel.snp.right) 55 | make.right.equalTo(rightLabel.snp.left) 56 | make.centerY.equalTo(self.contentView) 57 | } 58 | 59 | let topSeparator = UIImageView() 60 | self.contentView.addSubview(topSeparator) 61 | topSeparator.snp.makeConstraints{ (make) -> Void in 62 | make.left.right.top.equalTo(self.contentView) 63 | make.height.equalTo(SEPARATOR_HEIGHT) 64 | } 65 | 66 | let bottomSeparator = UIImageView() 67 | self.contentView.addSubview(bottomSeparator) 68 | bottomSeparator.snp.makeConstraints{ (make) -> Void in 69 | make.left.right.bottom.equalTo(self.contentView) 70 | make.height.equalTo(SEPARATOR_HEIGHT) 71 | } 72 | 73 | self.themeChangedHandler = {[weak self] (style) -> Void in 74 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 75 | leftLabel.textColor = V2EXColor.colors.v2_TopicListTitleColor 76 | rightLabel.textColor = V2EXColor.colors.v2_TopicListTitleColor 77 | topSeparator.image = createImageWithColor( V2EXColor.colors.v2_SeparatorColor ) 78 | bottomSeparator.image = topSeparator.image 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Common/V2Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2Style.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 3/10/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | //CSS基本样式 11 | private let BASE_CSS = try! String(contentsOfFile: Bundle.main.path(forResource: "baseStyle", ofType: "css")!, encoding: String.Encoding.utf8) 12 | //文字大小 13 | private let FONT_CSS = try! String(contentsOfFile: Bundle.main.path(forResource: "font", ofType: "css")!, encoding: String.Encoding.utf8) 14 | //暗色主题配色 15 | private let DARK_CSS = (try! String(contentsOfFile: Bundle.main.path(forResource: "darkStyle", ofType: "css")!, encoding: String.Encoding.utf8)) 16 | //亮色主题配色 17 | private let LIGHT_CSS = (try! String(contentsOfFile: Bundle.main.path(forResource: "lightStyle", ofType: "css")!, encoding: String.Encoding.utf8)) 18 | 19 | 20 | private let kFONTSCALE = "kFontScale" 21 | 22 | /// 自动维护APP的CSS文件 ,外界只需调用 V2Style.sharedInstance.CSS 即可取得APP所需要的CSS 23 | class V2Style: NSObject { 24 | static let sharedInstance = V2Style() 25 | 26 | fileprivate var _fontScale:Float = 1.0 27 | @objc dynamic var fontScale:Float { 28 | get{ 29 | return _fontScale 30 | } 31 | set{ 32 | if _fontScale != newValue { 33 | _fontScale = newValue 34 | self.remakeCSS() 35 | V2EXSettings.sharedInstance[kFONTSCALE] = "\(_fontScale)" 36 | } 37 | } 38 | } 39 | var CSS = "" 40 | 41 | fileprivate override init() { 42 | super.init() 43 | //加载字体大小设置 44 | if let fontScaleString:String = V2EXSettings.sharedInstance[kFONTSCALE] , let scale = Float(fontScaleString){ 45 | self._fontScale = scale 46 | } 47 | //监听主题配色,切换相应的配色 48 | self.themeChangedHandler = {[weak self] (style) -> Void in 49 | self?.remakeCSS() 50 | } 51 | 52 | } 53 | 54 | //重新拼接CSS字符串 55 | fileprivate func remakeCSS(){ 56 | if let _ = V2EXColor.colors as? V2EXDefaultColor { 57 | self.CSS = BASE_CSS + self.fontCss() + LIGHT_CSS 58 | } 59 | else{ 60 | self.CSS = BASE_CSS + self.fontCss() + DARK_CSS 61 | } 62 | } 63 | 64 | /** 65 | 获取 FONT_CSS 66 | */ 67 | fileprivate func fontCss() -> String { 68 | var fontCss = FONT_CSS 69 | 70 | //替换FONT_SIZE 71 | FONT_SIZE_ARRAY.forEach { (fontSize) -> () in 72 | fontCss = fontCss.replacingOccurrences(of: fontSize.labelName, with:String(fontSize.defaultFontSize * fontScale)) 73 | } 74 | 75 | return fontCss 76 | } 77 | } 78 | 79 | 80 | 81 | 82 | let FONT_SIZE_ARRAY = [ 83 | V2FontSize(labelName:"",defaultFontSize:18), 84 | V2FontSize(labelName:"",defaultFontSize:18), 85 | V2FontSize(labelName:"",defaultFontSize:16), 86 | V2FontSize(labelName:"",defaultFontSize:13), 87 | V2FontSize(labelName:"",defaultFontSize:14), 88 | V2FontSize(labelName:"",defaultFontSize:12), 89 | V2FontSize(labelName:"",defaultFontSize:10), 90 | ] 91 | 92 | struct V2FontSize { 93 | let labelName:String 94 | let defaultFontSize:Float 95 | } 96 | 97 | -------------------------------------------------------------------------------- /View/MemberHeaderCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberHeaderCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/1/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MemberHeaderCell: UITableViewCell { 12 | /// 头像 13 | var avatarImageView: UIImageView = { 14 | let avatarImageView = UIImageView() 15 | avatarImageView.backgroundColor = UIColor(white: 0.9, alpha: 0.3) 16 | avatarImageView.layer.borderWidth = 1.5 17 | avatarImageView.layer.borderColor = UIColor(white: 1, alpha: 0.6).cgColor 18 | avatarImageView.layer.masksToBounds = true 19 | avatarImageView.layer.cornerRadius = 38 20 | return avatarImageView 21 | }() 22 | /// 用户名 23 | var userNameLabel: UILabel = { 24 | let userNameLabel = UILabel() 25 | userNameLabel.textColor = UIColor(white: 0.85, alpha: 1) 26 | userNameLabel.font = v2Font(16) 27 | userNameLabel.text = "Hello" 28 | return userNameLabel 29 | }() 30 | /// 签名 31 | var introduceLabel: UILabel = { 32 | let introduceLabel = UILabel() 33 | introduceLabel.textColor = UIColor(white: 0.75, alpha: 1) 34 | introduceLabel.font = v2Font(16) 35 | introduceLabel.numberOfLines = 2 36 | introduceLabel.textAlignment = .center 37 | return introduceLabel 38 | }() 39 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 40 | super.init(style: style, reuseIdentifier: reuseIdentifier); 41 | self.setup(); 42 | } 43 | required init?(coder aDecoder: NSCoder) { 44 | super.init(coder: aDecoder) 45 | } 46 | func setup()->Void{ 47 | self.backgroundColor = UIColor.clear 48 | self.selectionStyle = .none 49 | 50 | self.contentView.addSubview(self.avatarImageView) 51 | self.contentView.addSubview(self.userNameLabel) 52 | self.contentView.addSubview(self.introduceLabel) 53 | 54 | self.setupLayout() 55 | } 56 | 57 | func setupLayout(){ 58 | self.avatarImageView.snp.makeConstraints{ (make) -> Void in 59 | make.centerX.equalTo(self.contentView) 60 | make.centerY.equalTo(self.contentView).offset(-15) 61 | make.width.height.equalTo(self.avatarImageView.layer.cornerRadius * 2) 62 | } 63 | self.userNameLabel.snp.makeConstraints{ (make) -> Void in 64 | make.top.equalTo(self.avatarImageView.snp.bottom).offset(10) 65 | make.centerX.equalTo(self.avatarImageView) 66 | } 67 | self.introduceLabel.snp.makeConstraints{ (make) -> Void in 68 | make.top.equalTo(self.userNameLabel.snp.bottom).offset(5) 69 | make.centerX.equalTo(self.avatarImageView) 70 | make.left.equalTo(self.contentView).offset(15) 71 | make.right.equalTo(self.contentView).offset(-15) 72 | } 73 | } 74 | 75 | func bind(_ model:MemberModel?){ 76 | if let model = model { 77 | if let avata = model.avata?.avatarString { 78 | self.avatarImageView.kf.setImage(with: URL(string: avata)!) 79 | } 80 | self.userNameLabel.text = model.userName; 81 | self.introduceLabel.text = model.introduce; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - 1PasswordExtension (1.8.5) 3 | - Alamofire (4.9.1) 4 | - AlamofireObjectMapper (5.2.1): 5 | - Alamofire (~> 4.7) 6 | - ObjectMapper (~> 3.4) 7 | - CXSwipeGestureRecognizer (1.1.0) 8 | - DrawerController (4.0.0): 9 | - DrawerController/Core (= 4.0.0) 10 | - DrawerController/DrawerVisualStates (= 4.0.0) 11 | - DrawerController/Core (4.0.0) 12 | - DrawerController/DrawerVisualStates (4.0.0): 13 | - DrawerController/Core 14 | - FDFullscreenPopGesture (1.1) 15 | - FXBlurView (1.6.4) 16 | - Ji (5.0.0) 17 | - KeychainSwift (18.0.0) 18 | - Kingfisher (7.11.0) 19 | - KVOController (1.2.0) 20 | - MJRefresh (3.1.15.7) 21 | - Moya/Core (13.0.1): 22 | - Alamofire (~> 4.1) 23 | - Result (~> 4.1) 24 | - Moya/RxSwift (13.0.1): 25 | - Moya/Core 26 | - RxSwift (~> 4.0) 27 | - ObjectMapper (3.5.1) 28 | - Result (4.1.0) 29 | - RxSwift (4.5.0) 30 | - Shimmer (1.0.2) 31 | - SnapKit (5.0.1) 32 | - SVProgressHUD (2.2.5) 33 | - SwiftyJSON (4.3.0) 34 | - YYText (1.0.7) 35 | 36 | DEPENDENCIES: 37 | - 1PasswordExtension 38 | - Alamofire 39 | - AlamofireObjectMapper 40 | - CXSwipeGestureRecognizer 41 | - DrawerController 42 | - FDFullscreenPopGesture 43 | - FXBlurView 44 | - Ji 45 | - KeychainSwift 46 | - Kingfisher (~> 7.11.0) 47 | - KVOController 48 | - MJRefresh (~> 3.1.15.7) 49 | - Moya/RxSwift 50 | - ObjectMapper 51 | - Shimmer 52 | - SnapKit 53 | - SVProgressHUD 54 | - SwiftyJSON (~> 4.3) 55 | - YYText 56 | 57 | SPEC REPOS: 58 | trunk: 59 | - 1PasswordExtension 60 | - Alamofire 61 | - AlamofireObjectMapper 62 | - CXSwipeGestureRecognizer 63 | - DrawerController 64 | - FDFullscreenPopGesture 65 | - FXBlurView 66 | - Ji 67 | - KeychainSwift 68 | - Kingfisher 69 | - KVOController 70 | - MJRefresh 71 | - Moya 72 | - ObjectMapper 73 | - Result 74 | - RxSwift 75 | - Shimmer 76 | - SnapKit 77 | - SVProgressHUD 78 | - SwiftyJSON 79 | - YYText 80 | 81 | SPEC CHECKSUMS: 82 | 1PasswordExtension: 0e95bdea64ec8ff2f4f693be5467a09fac42a83d 83 | Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18 84 | AlamofireObjectMapper: 1989f690e982b71921b9253f53a4f33a9bc00d88 85 | CXSwipeGestureRecognizer: 5a53916aa69e85d041c35b5fc2180ff0bfe6333b 86 | DrawerController: eb1168e9f7185cc49f654ae8486917f0acb3dc88 87 | FDFullscreenPopGesture: a8a620179e3d9c40e8e00256dcee1c1a27c6d0f0 88 | FXBlurView: db786c2561cb49a09ae98407f52460096ab8a44f 89 | Ji: d795fed288fe78658b404c88946d753b17d8d7f4 90 | KeychainSwift: c46e1438d121e47459fb304ac88c5e058a2a91ed 91 | Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac 92 | KVOController: d72ace34afea42468329623b3379ab3cd1d286b6 93 | MJRefresh: 697f8ec75ebdbe9207767bb682cf0f51b0d8a41f 94 | Moya: f4a4b80ff2f8a4ffc208dfb31cd91636622fee6e 95 | ObjectMapper: 70187b8941977c62ccfb423caf6b50be405cabf0 96 | Result: bd966fac789cc6c1563440b348ab2598cc24d5c7 97 | RxSwift: f172070dfd1a93d70a9ab97a5a01166206e1c575 98 | Shimmer: c5374be1c2b0c9e292fb05b339a513cf291cac86 99 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 100 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 101 | SwiftyJSON: 6faa0040f8b59dead0ee07436cbf76b73c08fd08 102 | YYText: 5c461d709e24d55a182d1441c41dc639a18a4849 103 | 104 | PODFILE CHECKSUM: 3ddddc85b80ca8760de77cc05b6b02dcb02f1ea1 105 | 106 | COCOAPODS: 1.16.2 107 | -------------------------------------------------------------------------------- /View/LeftNodeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftNodeTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/23/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LeftNodeTableViewCell: UITableViewCell { 12 | 13 | var nodeImageView: UIImageView = UIImageView() 14 | var nodeNameLabel: UILabel = { 15 | let label = UILabel() 16 | label.font = v2Font(16) 17 | return label 18 | }() 19 | var panel = UIView() 20 | 21 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 22 | super.init(style: style, reuseIdentifier: reuseIdentifier); 23 | self.setup(); 24 | } 25 | required init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | } 28 | 29 | func setup()->Void{ 30 | self.selectionStyle = .none 31 | self.backgroundColor = UIColor.clear 32 | 33 | self.contentView.addSubview(panel) 34 | panel.addSubview(self.nodeImageView) 35 | panel.addSubview(self.nodeNameLabel) 36 | 37 | panel.snp.makeConstraints{ (make) -> Void in 38 | make.left.top.right.equalTo(self.contentView) 39 | make.height.equalTo(55) 40 | } 41 | self.nodeImageView.snp.makeConstraints{ (make) -> Void in 42 | make.centerY.equalTo(panel) 43 | make.left.equalTo(panel).offset(20) 44 | make.width.height.equalTo(25) 45 | } 46 | self.nodeNameLabel.snp.makeConstraints{ (make) -> Void in 47 | make.left.equalTo(self.nodeImageView.snp.right).offset(20) 48 | make.centerY.equalTo(self.nodeImageView) 49 | } 50 | 51 | self.themeChangedHandler = {[weak self] (style) -> Void in 52 | self?.configureColor() 53 | } 54 | } 55 | func configureColor(){ 56 | self.panel.backgroundColor = V2EXColor.colors.v2_LeftNodeBackgroundColor 57 | self.nodeImageView.tintColor = V2EXColor.colors.v2_LeftNodeTintColor 58 | self.nodeNameLabel.textColor = V2EXColor.colors.v2_LeftNodeTintColor 59 | } 60 | } 61 | 62 | 63 | class LeftNotifictionCell : LeftNodeTableViewCell{ 64 | var notifictionCountLabel:UILabel = { 65 | let label = UILabel() 66 | label.font = v2Font(10) 67 | label.textColor = UIColor.white 68 | label.layer.cornerRadius = 7 69 | label.layer.masksToBounds = true 70 | label.backgroundColor = V2EXColor.colors.v2_NoticePointColor 71 | return label 72 | }() 73 | 74 | override func setup() { 75 | super.setup() 76 | self.nodeNameLabel.text = NSLocalizedString("notifications") 77 | 78 | self.contentView.addSubview(self.notifictionCountLabel) 79 | self.notifictionCountLabel.snp.makeConstraints{ (make) -> Void in 80 | make.centerY.equalTo(self.nodeNameLabel) 81 | make.left.equalTo(self.nodeNameLabel.snp.right).offset(5) 82 | make.height.equalTo(14) 83 | } 84 | 85 | self.kvoController.observe(V2User.sharedInstance, keyPath: "notificationCount", options: [.initial,.new]) { [weak self](cell, clien, change) -> Void in 86 | if V2User.sharedInstance.notificationCount > 0 { 87 | self?.notifictionCountLabel.text = " \(V2User.sharedInstance.notificationCount) " 88 | } 89 | else{ 90 | self?.notifictionCountLabel.text = "" 91 | } 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /View/AccountListTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountListTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/11/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AccountListTableViewCell: UITableViewCell { 12 | var avatarImageView:UIImageView = { 13 | let avatarImageView = UIImageView() 14 | avatarImageView.backgroundColor = UIColor(white: 0.9, alpha: 0.3) 15 | avatarImageView.layer.masksToBounds = true 16 | avatarImageView.layer.cornerRadius = 22 17 | return avatarImageView 18 | }() 19 | var userNameLabel:UILabel = { 20 | let userNameLabel = UILabel() 21 | userNameLabel.font = v2Font(14) 22 | return userNameLabel 23 | }() 24 | var usedLabel:UILabel = { 25 | let usedLabel = UILabel() 26 | usedLabel.font = v2Font(11) 27 | usedLabel.text = NSLocalizedString("current") 28 | return usedLabel 29 | }() 30 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 31 | super.init(style: style, reuseIdentifier: reuseIdentifier); 32 | self.setup(); 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | func setup()->Void{ 39 | self.selectionStyle = .none 40 | 41 | self.contentView.addSubview(self.avatarImageView) 42 | self.contentView.addSubview(self.userNameLabel) 43 | self.contentView.addSubview(self.usedLabel) 44 | let separator = UIImageView() 45 | self.contentView.addSubview(separator) 46 | 47 | self.usedLabel.isHidden = true; 48 | 49 | self.avatarImageView.snp.makeConstraints{ (make) -> Void in 50 | make.left.equalTo(self.contentView).offset(15) 51 | make.centerY.equalTo(self.contentView) 52 | make.width.height.equalTo(self.avatarImageView.layer.cornerRadius * 2) 53 | } 54 | self.userNameLabel.snp.makeConstraints{ (make) -> Void in 55 | make.left.equalTo(self.avatarImageView.snp.right).offset(15) 56 | make.centerY.equalTo(self.avatarImageView) 57 | } 58 | self.usedLabel.snp.makeConstraints{ (make) -> Void in 59 | make.right.equalTo(self.contentView).offset(-15) 60 | make.centerY.equalTo(self.avatarImageView) 61 | } 62 | separator.snp.makeConstraints{ (make) -> Void in 63 | make.left.equalTo(self.avatarImageView.snp.right).offset(5) 64 | make.right.bottom.equalTo(self.contentView) 65 | make.height.equalTo(SEPARATOR_HEIGHT) 66 | } 67 | 68 | self.themeChangedHandler = {[weak self] _ in 69 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 70 | self?.userNameLabel.textColor = V2EXColor.colors.v2_TopicListUserNameColor 71 | self?.usedLabel.textColor = V2EXColor.colors.v2_NoticePointColor 72 | 73 | separator.image = createImageWithColor(V2EXColor.colors.v2_SeparatorColor) 74 | } 75 | } 76 | 77 | func bind(_ model:LocalSecurityAccountModel) { 78 | self.userNameLabel.text = model.username 79 | if let avatar = model.avatar , let url = URL(string: avatar) { 80 | self.avatarImageView.fin_setImageWithUrl(url) 81 | } 82 | if V2User.sharedInstance.username == model.username { 83 | self.usedLabel.isHidden = false 84 | } 85 | else { 86 | self.usedLabel.isHidden = true 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /V2ex-Swift.xcodeproj/xcshareddata/xcschemes/V2ex-Swift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Model/Moya/V2EXTargetType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2EXTargetType.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2017/5/24. 6 | // Copyright © 2017年 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Moya 11 | import RxSwift 12 | import Result 13 | 14 | //保存全局Providers 15 | fileprivate var retainProviders:[String: Any] = [:] 16 | 17 | protocol V2EXTargetType: TargetType { 18 | var parameters: [String: Any]? { get } 19 | } 20 | 21 | extension V2EXTargetType { 22 | var headers: [String : String]? { 23 | return MOBILE_CLIENT_HEADERS 24 | } 25 | var baseURL: URL { 26 | return URL(string: "https://www.v2ex.com")! 27 | } 28 | 29 | var method: Moya.Method { 30 | return .get 31 | } 32 | 33 | var parameterEncoding: ParameterEncoding { 34 | return URLEncoding.default 35 | } 36 | 37 | var sampleData: Data { 38 | return Data() 39 | } 40 | 41 | var task: Task { 42 | return requestTaskWithParameters 43 | } 44 | 45 | var requestTaskWithParameters: Task { 46 | get { 47 | //默认参数 48 | var defaultParameters:[String:Any] = [:] 49 | //协议参数 50 | if let parameters = self.parameters { 51 | for (key, value) in parameters { 52 | defaultParameters[key] = value 53 | } 54 | } 55 | return Task.requestParameters(parameters: defaultParameters, encoding: parameterEncoding) 56 | } 57 | } 58 | 59 | static var networkActivityPlugin: PluginType { 60 | return NetworkActivityPlugin { (change, type) in 61 | switch change { 62 | case .began: 63 | UIApplication.shared.isNetworkActivityIndicatorVisible = true 64 | case .ended: 65 | UIApplication.shared.isNetworkActivityIndicatorVisible = false 66 | } 67 | } 68 | } 69 | 70 | /// 实现此协议的类,将自动获得用该类实例化的 provider 对象 71 | static var provider: RxSwift.Reactive< MoyaProvider > { 72 | let key = "\(Self.self)" 73 | if let provider = retainProviders[key] as? RxSwift.Reactive< MoyaProvider > { 74 | return provider 75 | } 76 | let provider = Self.weakProvider 77 | retainProviders[key] = provider 78 | return provider 79 | } 80 | 81 | /// 不被全局持有的 Provider ,使用时,需要持有它,否则将立即释放,请求随即终止 82 | static var weakProvider: RxSwift.Reactive< MoyaProvider > { 83 | var plugins:[PluginType] = [networkActivityPlugin] 84 | #if DEBUG 85 | plugins.append(LogPlugin()) 86 | #endif 87 | let provider = MoyaProvider(plugins:plugins) 88 | return provider.rx 89 | } 90 | } 91 | 92 | extension RxSwift.Reactive where Base: MoyaProviderType { 93 | public func requestAPI(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Observable { 94 | return self.request(token, callbackQueue: callbackQueue).asObservable() 95 | } 96 | } 97 | 98 | fileprivate class LogPlugin: PluginType{ 99 | func willSend(_ request: RequestType, target: TargetType) { 100 | print("\n-------------------\n准备请求: \(target.path)") 101 | print("请求方式: \(target.method.rawValue)") 102 | if let params = (target as? V2EXTargetType)?.parameters { 103 | print(params) 104 | } 105 | print("\n") 106 | 107 | } 108 | func didReceive(_ result: Result, target: TargetType) { 109 | print("\n-------------------\n请求结束: \(target.path)") 110 | if let data = result.value?.data, let resutl = String(data: data, encoding: String.Encoding.utf8) { 111 | print("请求结果: \(resutl)") 112 | } 113 | print("\n") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /View/V2PhotoBrowser/V2PhotoBrowserTransionDismiss.swift: -------------------------------------------------------------------------------- 1 | // 2 | // V2PhotoBrowserTransionDismiss.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/26/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class V2PhotoBrowserTransionDismiss:NSObject,UIViewControllerAnimatedTransitioning { 12 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 13 | let fromVC = transitionContext!.viewController(forKey: UITransitionContextViewControllerKey.from) as! V2PhotoBrowser 14 | if fromVC.transitionController.interacting { 15 | return 0.8 16 | } 17 | else{ 18 | return 0.3 19 | } 20 | } 21 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 22 | 23 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! V2PhotoBrowser 24 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)! 25 | 26 | let container = transitionContext.containerView 27 | container.addSubview(toVC.view) 28 | container.bringSubviewToFront(fromVC.view) 29 | 30 | 31 | if let delegate = fromVC.delegate ,let image = delegate.guideImageInPhotoBrowser(fromVC, index: fromVC.currentPageIndex) { 32 | fromVC.guideImageView.image = image 33 | //如果图片过小,则直接中间原图显示 ,否则fit 34 | if (fromVC.guideImageView.originalImage?.size.width)! > SCREEN_WIDTH || (fromVC.guideImageView.originalImage?.size.height)! > SCREEN_HEIGHT { 35 | fromVC.guideImageView.contentMode = .scaleAspectFit 36 | } 37 | else{ 38 | fromVC.guideImageView.contentMode = .center 39 | } 40 | 41 | //重布局一下,因为有可能左右切换图片隐藏时 布局不对 42 | fromVC.guideImageView.setNeedsLayout() 43 | fromVC.guideImageView.layoutIfNeeded() 44 | } 45 | 46 | //显示引导动画,隐藏真正的照片浏览器 47 | //如果引导动画的图片没有加载完或加载失败,则显示真正的照片浏览器 渐变隐藏它 48 | fromVC.guideImageViewHidden(false) 49 | 50 | let animations = { () -> Void in 51 | fromVC.view.backgroundColor = UIColor(white: 0, alpha: 0) 52 | //如果guideImageView是隐藏的,则证明图片没有加载完不能显示,则渐变隐藏整个browser 53 | if fromVC.guideImageView.isHidden { 54 | fromVC.pagingScrollView.alpha = 0 55 | } 56 | else { 57 | if !fromVC.transitionController.interacting { 58 | if let delegate = fromVC.delegate { 59 | fromVC.guideImageView.frame = delegate.guideFrameInPhotoBrowser(fromVC, index: fromVC.currentPageIndex) 60 | fromVC.guideImageView.contentMode = delegate.guideContentModeInPhotoBrowser(fromVC, index: fromVC.currentPageIndex) 61 | } 62 | } 63 | else{ 64 | var frame = fromVC.guideImageView.frame 65 | if fromVC.transitionController.direction == .downwards { 66 | frame.origin.y += fromVC.view.frame.size.height 67 | } 68 | else { 69 | frame.origin.y += 0 - frame.size.height 70 | } 71 | fromVC.guideImageView.frame = frame 72 | } 73 | } 74 | } 75 | 76 | let completion = {(finished: Bool) -> Void in 77 | fromVC.guideImageViewHidden(true) 78 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 79 | } 80 | 81 | let options = fromVC.transitionController.interacting ? UIView.AnimationOptions.curveLinear : UIView.AnimationOptions() 82 | 83 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: options, animations: animations, completion: completion) 84 | } 85 | 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /Controller/MoreViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoreViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/30/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MoreViewController: UITableViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | self.title = NSLocalizedString("more") 16 | 17 | self.tableView.separatorStyle = .none; 18 | regClass(self.tableView, cell: BaseDetailTableViewCell.self) 19 | 20 | self.themeChangedHandler = {[weak self] (style) -> Void in 21 | self?.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 22 | self?.tableView.reloadData() 23 | } 24 | } 25 | 26 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ 27 | return 10 28 | } 29 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 30 | return [30,50,12,50,50,50,12,50,50,50][indexPath.row] 31 | } 32 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | let cell = getCell(tableView, cell: BaseDetailTableViewCell.self, indexPath: indexPath) 34 | cell.selectionStyle = .none 35 | 36 | //设置标题 37 | cell.titleLabel.text = [ 38 | "", 39 | NSLocalizedString("viewOptions"), 40 | "", 41 | NSLocalizedString("rateV2ex"), 42 | NSLocalizedString("reportAProblem"), 43 | NSLocalizedString("userAgreement"), 44 | "", 45 | NSLocalizedString("followThisProjectSourceCode"), 46 | NSLocalizedString("open-SourceLibraries"), 47 | NSLocalizedString("version")][indexPath.row] 48 | 49 | //设置颜色 50 | if [0,2,6].contains(indexPath.row) { 51 | cell.backgroundColor = self.tableView.backgroundColor 52 | } 53 | else{ 54 | cell.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 55 | } 56 | 57 | //设置右侧箭头 58 | if [0,2,6,9].contains(indexPath.row) { 59 | cell.detailMarkHidden = true 60 | } 61 | else { 62 | cell.detailMarkHidden = false 63 | } 64 | 65 | //设置右侧文本 66 | if indexPath.row == 9 { 67 | cell.detailLabel.text = "Version " + (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String) 68 | + " (Build " + (Bundle.main.infoDictionary!["CFBundleVersion"] as! String ) + ")" 69 | } 70 | else { 71 | cell.detailLabel.text = "" 72 | } 73 | 74 | 75 | return cell 76 | } 77 | 78 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 79 | if indexPath.row == 1 { 80 | V2Client.sharedInstance.centerNavigation?.pushViewController(SettingsTableViewController(), animated: true) 81 | } 82 | else if indexPath.row == 3 { 83 | let str = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1078157349" 84 | UIApplication.shared.open(URL(string: str)!) 85 | } 86 | else if indexPath.row == 4 { 87 | UIApplication.shared.open(URL(string: "https://day.app/2016/02/v2ex-ioske-hu-duan-bug-and-jian-yi/")!) 88 | } 89 | else if indexPath.row == 5 { 90 | self.navigationController?.pushViewController(AgreementViewController(), animated: true) 91 | } 92 | else if indexPath.row == 7 { 93 | UIApplication.shared.open(URL(string: "https://github.com/Finb/V2ex-Swift")!) 94 | } 95 | else if indexPath.row == 8 { 96 | V2Client.sharedInstance.centerNavigation?.pushViewController(PodsTableViewController(), animated: true) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /View/BaseDetailTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDetailTableViewCell.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/21/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseDetailTableViewCell: UITableViewCell { 12 | var titleLabel:UILabel = { 13 | let label = UILabel() 14 | label.font = v2Font(16) 15 | return label 16 | }() 17 | 18 | var detailLabel:UILabel = { 19 | let label = UILabel() 20 | label.font = v2Font(13) 21 | return label 22 | }() 23 | 24 | var detailMarkImageView:UIImageView = { 25 | let imageview = UIImageView(image: UIImage.imageUsedTemplateMode("ic_keyboard_arrow_right")) 26 | imageview.contentMode = .center 27 | return imageview 28 | }() 29 | 30 | var separator:UIImageView = UIImageView() 31 | 32 | var detailMarkHidden:Bool { 33 | get{ 34 | return self.detailMarkImageView.isHidden 35 | } 36 | 37 | set{ 38 | if self.detailMarkImageView.isHidden == newValue{ 39 | return ; 40 | } 41 | 42 | self.detailMarkImageView.isHidden = newValue 43 | if newValue { 44 | self.detailMarkImageView.snp.remakeConstraints{ (make) -> Void in 45 | make.width.height.equalTo(0) 46 | make.centerY.equalTo(self.contentView) 47 | make.right.equalTo(self.contentView).offset(-12) 48 | } 49 | } 50 | else{ 51 | self.detailMarkImageView.snp.remakeConstraints{ (make) -> Void in 52 | make.width.height.equalTo(20) 53 | make.centerY.equalTo(self.contentView) 54 | make.right.equalTo(self.contentView).offset(-12) 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 62 | super.init(style: style, reuseIdentifier: reuseIdentifier); 63 | self.setup(); 64 | } 65 | required init?(coder aDecoder: NSCoder) { 66 | super.init(coder: aDecoder) 67 | } 68 | 69 | func setup()->Void{ 70 | let selectedBackgroundView = UIView() 71 | self.selectedBackgroundView = selectedBackgroundView 72 | 73 | self.contentView.addSubview(self.titleLabel) 74 | self.contentView.addSubview(self.detailMarkImageView); 75 | self.contentView.addSubview(self.detailLabel) 76 | self.contentView.addSubview(self.separator) 77 | 78 | 79 | self.titleLabel.snp.makeConstraints{ (make) -> Void in 80 | make.left.equalTo(self.contentView).offset(12) 81 | make.centerY.equalTo(self.contentView) 82 | } 83 | self.detailMarkImageView.snp.remakeConstraints{ (make) -> Void in 84 | make.height.equalTo(24) 85 | make.width.equalTo(14) 86 | make.centerY.equalTo(self.contentView) 87 | make.right.equalTo(self.contentView).offset(-12) 88 | } 89 | self.detailLabel.snp.makeConstraints{ (make) -> Void in 90 | make.right.equalTo(self.detailMarkImageView.snp.left).offset(-5) 91 | make.centerY.equalTo(self.contentView) 92 | } 93 | self.separator.snp.makeConstraints{ (make) -> Void in 94 | make.left.right.bottom.equalTo(self.contentView) 95 | make.height.equalTo(SEPARATOR_HEIGHT) 96 | } 97 | 98 | 99 | self.themeChangedHandler = {[weak self] (style) -> Void in 100 | self?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 101 | self?.selectedBackgroundView!.backgroundColor = V2EXColor.colors.v2_backgroundColor 102 | self?.titleLabel.textColor = V2EXColor.colors.v2_TopicListTitleColor 103 | self?.detailMarkImageView.tintColor = self?.titleLabel.textColor 104 | self?.detailLabel.textColor = V2EXColor.colors.v2_TopicListUserNameColor 105 | self?.separator.image = createImageWithColor( V2EXColor.colors.v2_SeparatorColor ) 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Controller/NotificationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/29/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MJRefresh 11 | class NotificationsViewController: BaseViewController,UITableViewDataSource,UITableViewDelegate { 12 | 13 | fileprivate var notificationsArray:[NotificationsModel] = [] 14 | 15 | fileprivate lazy var tableView: UITableView = { 16 | let tableView = UITableView() 17 | tableView.backgroundColor = UIColor.clear 18 | tableView.estimatedRowHeight = 100 19 | tableView.separatorStyle = .none 20 | 21 | regClass(tableView, cell: NotificationTableViewCell.self) 22 | 23 | tableView.delegate = self 24 | tableView.dataSource = self 25 | return tableView 26 | }() 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | self.view.addSubview(self.tableView); 31 | self.title = NSLocalizedString("notifications") 32 | 33 | self.tableView.snp.makeConstraints{ (make) -> Void in 34 | make.top.right.bottom.left.equalTo(self.view); 35 | } 36 | 37 | self.tableView.mj_header = V2RefreshHeader(refreshingBlock:{[weak self] () -> Void in 38 | self?.refresh() 39 | }) 40 | self.showLoadingView() 41 | self.tableView.mj_header.beginRefreshing(); 42 | 43 | self.themeChangedHandler = {[weak self] _ in 44 | self?.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 45 | self?.tableView.backgroundColor = V2EXColor.colors.v2_backgroundColor 46 | } 47 | } 48 | 49 | 50 | 51 | func refresh(){ 52 | NotificationsModel.getNotifications {[weak self] (response) -> Void in 53 | if response.success && response.value != nil { 54 | if let weakSelf = self{ 55 | weakSelf.notificationsArray = response.value! 56 | weakSelf.tableView.fin_reloadData() 57 | } 58 | } 59 | self?.tableView.mj_header.endRefreshing() 60 | self?.hideLoadingView() 61 | } 62 | } 63 | 64 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 65 | return self.notificationsArray.count 66 | } 67 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 68 | return tableView.fin_heightForCellWithIdentifier(NotificationTableViewCell.self, indexPath: indexPath) { (cell) -> Void in 69 | cell.bind(self.notificationsArray[indexPath.row]); 70 | } 71 | } 72 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 73 | let cell = getCell(tableView, cell: NotificationTableViewCell.self, indexPath: indexPath) 74 | cell.bind(self.notificationsArray[indexPath.row]) 75 | cell.replyButton.tag = indexPath.row 76 | if cell.replyButtonClickHandler == nil { 77 | cell.replyButtonClickHandler = { [weak self] (sender) in 78 | self?.replyClick(sender) 79 | } 80 | } 81 | return cell 82 | } 83 | 84 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 85 | let item = self.notificationsArray[indexPath.row] 86 | if let id = item.topicId { 87 | let topicDetailController = TopicDetailViewController(); 88 | topicDetailController.topicId = id ; 89 | self.navigationController?.pushViewController(topicDetailController, animated: true) 90 | tableView .deselectRow(at: indexPath, animated: true); 91 | } 92 | } 93 | 94 | func replyClick(_ sender:UIButton) { 95 | let item = self.notificationsArray[sender.tag] 96 | 97 | let replyViewController = ReplyingViewController() 98 | 99 | let tempTopicModel = TopicDetailModel() 100 | replyViewController.atSomeone = "@" + (item.userName ?? " ") 101 | tempTopicModel.topicId = item.topicId 102 | replyViewController.topicModel = tempTopicModel 103 | 104 | let nav = V2EXNavigationController(rootViewController:replyViewController) 105 | self.navigationController?.present(nav, animated: true, completion:nil) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Controller/WritingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WritingViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 1/25/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import YYText 11 | 12 | 13 | class WritingViewController: UIViewController ,UITextViewDelegate { 14 | 15 | var textView:UITextView = { 16 | let textView = UITextView() 17 | textView.scrollsToTop = false 18 | textView.backgroundColor = V2EXColor.colors.v2_TextViewBackgroundColor 19 | textView.font = v2Font(18) 20 | textView.textColor = V2EXColor.colors.v2_TopicListUserNameColor 21 | textView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 22 | textView.keyboardAppearance = V2EXColor.sharedInstance.style == V2EXColor.V2EXColorStyleDefault ? .default : .dark; 23 | return textView 24 | }() 25 | var topicModel :TopicDetailModel? 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | self.title = "写东西" 30 | self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(WritingViewController.leftClick)) 31 | 32 | let rightButton = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) 33 | rightButton.contentMode = .center 34 | rightButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -20) 35 | rightButton.setImage(UIImage(named: "ic_send")!.withRenderingMode(.alwaysTemplate), for: .normal) 36 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightButton) 37 | rightButton.addTarget(self, action: #selector(WritingViewController.rightClick), for: .touchUpInside) 38 | 39 | self.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 40 | 41 | self.textView.delegate = self 42 | self.view.addSubview(self.textView) 43 | self.textView.snp.makeConstraints{ (make) -> Void in 44 | make.top.right.bottom.left.equalTo(self.view) 45 | } 46 | 47 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 48 | } 49 | @objc func keyboardWillChangeFrame(notification: Notification) { 50 | guard let bound = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { 51 | return 52 | } 53 | self.textView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bound.size.height, right: 0) 54 | } 55 | @objc func leftClick (){ 56 | self.dismiss(animated: true, completion: nil) 57 | } 58 | @objc func rightClick (){ 59 | 60 | } 61 | 62 | func textViewDidChange(_ textView: UITextView) { 63 | if textView.text.Lenght == 0{ 64 | textView.textColor = V2EXColor.colors.v2_TopicListUserNameColor 65 | } 66 | } 67 | } 68 | 69 | class ReplyingViewController:WritingViewController { 70 | var atSomeone:String? 71 | override func viewDidLoad() { 72 | super.viewDidLoad() 73 | self.title = NSLocalizedString("reply") 74 | if let atSomeone = self.atSomeone { 75 | let str = NSMutableAttributedString(string: atSomeone + " ") 76 | str.yy_font = self.textView.font 77 | str.yy_color = self.textView.textColor 78 | str.yy_setColor(colorWith255RGB(0, g: 132, b: 255), range: NSMakeRange(0, str.length - 1)) 79 | str.yy_setAttribute("someoneEnd", value: 1, range:NSMakeRange(str.length - 1, 1)) 80 | 81 | self.textView.attributedText = str 82 | 83 | self.textView.selectedRange = NSMakeRange(str.length, 0); 84 | } 85 | } 86 | 87 | override func viewDidAppear(_ animated: Bool) { 88 | self.textView.becomeFirstResponder() 89 | } 90 | 91 | override func rightClick (){ 92 | guard let text = self.textView.text, text.Lenght > 0 else { 93 | return 94 | } 95 | V2ProgressHUD.showWithClearMask() 96 | TopicCommentModel.replyWithTopicId(self.topicModel!, content: text ) { 97 | (response) in 98 | if response.success { 99 | V2Success("回复成功!") 100 | self.dismiss(animated: true, completion: nil) 101 | } 102 | else{ 103 | V2Error(response.message) 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Controller/NodesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodesViewController.swift 3 | // V2ex-Swift 4 | // 5 | // Created by huangfeng on 2/2/16. 6 | // Copyright © 2016 Fin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | class NodesViewController: BaseViewController { 11 | var nodeGroupArray:[NodeGroupModel]? 12 | var collectionView:UICollectionView? 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.title = NSLocalizedString("Navigation") 16 | 17 | let layout = V2LeftAlignedCollectionViewFlowLayout(); 18 | layout.sectionInset = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15); 19 | self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) 20 | self.collectionView!.dataSource = self 21 | self.collectionView!.delegate = self 22 | self.view.addSubview(self.collectionView!) 23 | 24 | self.collectionView!.register(NodeTableViewCell.self, forCellWithReuseIdentifier: "cell") 25 | self.collectionView!.register(NodeCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "nodeGroupNameView") 26 | 27 | NodeGroupModel.getNodes { (response) -> Void in 28 | if response.success { 29 | self.nodeGroupArray = response.value 30 | self.collectionView?.reloadData() 31 | } 32 | self.hideLoadingView() 33 | } 34 | self.showLoadingView() 35 | 36 | self.themeChangedHandler = {[weak self] _ in 37 | self?.view.backgroundColor = V2EXColor.colors.v2_backgroundColor 38 | self?.collectionView?.backgroundColor = V2EXColor.colors.v2_CellWhiteBackgroundColor 39 | } 40 | } 41 | } 42 | 43 | 44 | //MARK: - UICollectionViewDataSource 45 | extension NodesViewController : UICollectionViewDataSource { 46 | func numberOfSections(in collectionView: UICollectionView) -> Int { 47 | if let count = self.nodeGroupArray?.count{ 48 | return count 49 | } 50 | return 0 51 | } 52 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 53 | return self.nodeGroupArray![section].children.count 54 | } 55 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let nodeModel = self.nodeGroupArray![indexPath.section].children[indexPath.row] 57 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NodeTableViewCell; 58 | cell.textLabel.text = nodeModel.nodeName 59 | return cell; 60 | } 61 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 62 | let nodeGroupNameView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "nodeGroupNameView", for: indexPath) 63 | (nodeGroupNameView as! NodeCollectionReusableView).label.text = self.nodeGroupArray![indexPath.section].groupName 64 | return nodeGroupNameView 65 | } 66 | } 67 | 68 | 69 | //MARK: - UICollectionViewDelegateFlowLayout 70 | extension NodesViewController : UICollectionViewDelegateFlowLayout { 71 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 72 | let nodeModel = self.nodeGroupArray![indexPath.section].children[indexPath.row] 73 | return CGSize(width: nodeModel.width, height: 25); 74 | } 75 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat{ 76 | return 15 77 | } 78 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 79 | return CGSize(width: collectionView.bounds.size.width, height: 35); 80 | } 81 | } 82 | 83 | 84 | //MARK: - UICollectionViewDelegate 85 | extension NodesViewController : UICollectionViewDelegate { 86 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath){ 87 | let nodeModel = self.nodeGroupArray![indexPath.section].children[indexPath.row] 88 | let controller = NodeTopicListViewController() 89 | controller.node = nodeModel 90 | V2Client.sharedInstance.centerNavigation?.pushViewController(controller, animated: true) 91 | } 92 | } 93 | --------------------------------------------------------------------------------