├── 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 | [](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 | 
39 | 
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 | 
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 |
--------------------------------------------------------------------------------