Commit d68129c2 authored by yqz's avatar yqz

iniit

parent 7dadafe9
......@@ -12,7 +12,7 @@ target 'SpeakEasyLearnEnglish' do
pod 'lottie-ios', '~> 4.5.2'
pod 'SnapKit', '~> 5.7.1'
pod 'YYText'
pod 'TPKeyboardAvoiding' , '~> 1.3.5'
post_install do |installer|
installer.pods_project.targets.each do |target|
......
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Send@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Send@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon Name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Icon Name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon Name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Icon Name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
//
// AppDelegate+LoadData.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
extension AppDelegate {
func initData() -> Void {
SpeakKeyboardManager.share.monitor()
_ = SpeakVideoPlayer.share
SpeakElePublicManager.share.loadData()
}
}
......@@ -15,7 +15,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
_ = SpeakVideoPlayer.share
initData()
#if DEBUG
// // 获取所有可用字体家族名称
......@@ -45,6 +46,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
......@@ -12,6 +12,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
internal var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = SpeakEleLaunchViewCtr()
......@@ -34,6 +35,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
func sceneWillResignActive(_ scene: UIScene) {
saveData()
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
......@@ -49,6 +51,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// to restore the scene back to its current state.
}
func saveData() -> Void {
SpeakElePublicManager.share.saveData()
}
}
......@@ -44,4 +44,37 @@ class SpeakEleBaseNavigationCtr: UINavigationController, UIGestureRecognizerDele
return true
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if toVC.navigationController?.AnimationState == .present {
return SpeakCustomPushAnimator()
}
return nil
}
}
fileprivate var state = SpeakEasyAssociatedKeys.navigationAniKey.rawValue
extension UINavigationController {
enum navigationState : Int {
case none = 0
case present = 1
}
var AnimationState:navigationState {
set {
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let animation = objc_getAssociatedObject(self, &state) as? navigationState ?? .none
return animation
}
}
}
......@@ -16,19 +16,25 @@ let isDebug = false
let marginLR = 24
// MARK: - 所有分类属性 Key
enum SpeakEasyAssociatedKeys : UInt32 {
case fontsize = 0x101010
case UnsafeCallBack = 0x10000
case labelTextTap = 0x10001
case navigationAniKey = 0x01122
case labelTextCutomFont = 0x01102
}
protocol CutomFontProtocol {
protocol CutomFontProtocol : NSObjectProtocol {
var fontSize:CGFloat { get set }
var cutomFont:Int { get }
}
func setCutomFonts() -> Void
}
// MARK: - userdefaultKey
enum UnsafeRawUserDefaultsKey:String {
case UnsafeThreeDayGuide = "UnsafeThreeDayGuide"
case UnsafePublicDataKey = "UnsafePublicDataKey"
}
......@@ -10,6 +10,14 @@ import UIKit
extension String {
func localJson(_ fileName:String) -> String? {
let path = Bundle.main.path(forResource: fileName, ofType: "json")
return path
}
func attributed() -> AttributedStringBuilder {
return AttributedStringBuilder(self)
}
......
......@@ -9,22 +9,9 @@ import UIKit
extension UIButton : CutomFontProtocol {
@IBInspectable var fontSize:CGFloat {
set {
var state = SpeakEasyAssociatedKeys.fontsize.rawValue
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
var state = SpeakEasyAssociatedKeys.fontsize.rawValue
let size = objc_getAssociatedObject(self, &state) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
switch newValue {
func setCutomFonts() {
if cutomFont >= 0 {
switch cutomFont {
case 0:
self.titleLabel?.font = UIFont.montserrat(.regular ,size: fontSize)
break
......@@ -39,10 +26,6 @@ extension UIButton : CutomFontProtocol {
break
}
}
get {
return 0
}
}
}
......@@ -8,11 +8,12 @@
import UIKit
import CoreText
fileprivate var state = SpeakEasyAssociatedKeys.fontsize.rawValue
fileprivate var textTapState = SpeakEasyAssociatedKeys.labelTextTap.rawValue
@IBDesignable
extension UILabel : CutomFontProtocol{
extension UILabel : CutomFontProtocol {
/// 设置文字间距
func lineSpacing(_ lineSpacing:CGFloat = 5) {
......@@ -24,19 +25,10 @@ extension UILabel : CutomFontProtocol{
}
@IBInspectable var fontSize:CGFloat {
set {
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let size = objc_getAssociatedObject(self, &state) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
switch newValue {
func setCutomFonts() {
if cutomFont >= 0 {
switch cutomFont {
case 0:
self.font = UIFont.montserrat(.regular ,size: fontSize)
break
......@@ -51,11 +43,7 @@ extension UILabel : CutomFontProtocol{
break
}
}
get {
return 0
}
}
var textTaps:[String] {
set {
......
......@@ -8,6 +8,9 @@
import UIKit
fileprivate var state = SpeakEasyAssociatedKeys.UnsafeCallBack.rawValue
fileprivate var fontState = SpeakEasyAssociatedKeys.fontsize.rawValue
fileprivate var cutomFontsKey = SpeakEasyAssociatedKeys.labelTextCutomFont.rawValue
@IBDesignable
extension UIView {
......@@ -107,6 +110,26 @@ extension UIView {
}
}
@IBInspectable var fontSize:CGFloat {
set {
objc_setAssociatedObject(self, &fontState, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let size = objc_getAssociatedObject(self, &fontState) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
objc_setAssociatedObject(self, &cutomFontsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &cutomFontsKey) as? Int ?? -1
}
}
/** 获取当前控制器 */
func responderCtr() -> UIViewController? {
for view in sequence(first: self.superview, next: { $0?.superview }) {
......
//
// SpeakElePublicManager.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakElePublicManager: NSObject {
static let share = SpeakElePublicManager()
var PublicData:PublicModel = PublicModel()
func loadData() -> Void {
do {
let manager = UserDefaults.standard.object(forKey: UnsafeRawUserDefaultsKey.UnsafePublicDataKey.rawValue) as? Data ?? Data()
let data = try JSONDecoder().decode(PublicModel.self, from: manager)
PublicData = data
}catch{
}
}
func saveData() -> Void {
do{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(PublicData)
UserDefaults.standard.set(data, forKey: UnsafeRawUserDefaultsKey.UnsafePublicDataKey.rawValue)
UserDefaults.standard.synchronize()
}catch {
}
}
private override init() { }
}
enum guideState:Int,Codable{
/// 开始
case start = 0
/// 订阅
case subscribe = 1
/// 问答
case qanda = 2
/// 登录
case login = 3
/// 首页
case home = 4
}
struct PublicModel : Codable {
var state:guideState = .start
}
......@@ -28,18 +28,18 @@ class SpeakEleGuideViewCtr: SpeakEleBaseViewCtr ,UIScrollViewDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
titleView.isHidden = true
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
@IBAction func SpeakNextTaps(_ sender: Any) {
if selectIdx >= 2 {
if let isThree = UserDefaults.standard.object(forKey: UnsafeRawUserDefaultsKey.UnsafeThreeDayGuide.rawValue) as? Bool {
if isThree {
SpeakElePublicManager.share.PublicData.state = .subscribe
let iap = SpeakEleIAPViewCtr()
iap.modalPresentationStyle = .overFullScreen
self.present(iap, animated: true)
iap.callback = { [weak self] in
let QAndA = SpeakEleQAViewCtr()
self?.navigationController?.pushViewController(QAndA, animated: true)
self.navigationController?.AnimationState = .present
self.navigationController?.pushViewController(iap, animated: true)
}
}else {
UserDefaults.standard.set(true, forKey: UnsafeRawUserDefaultsKey.UnsafeThreeDayGuide.rawValue)
......
......@@ -9,8 +9,46 @@ import UIKit
class SpeakEleQAViewCtr: SpeakEleBaseViewCtr {
// MARK: - ib
@IBOutlet weak var SpeakProgres: UISlider!
@IBOutlet weak var SpeakContentV: UIView!
@IBOutlet weak var SpeakTableView: UITableView!
@IBOutlet weak var SpeakInputBox: SpeakResizableTextView!
private let viewModel = SpeakEleQAViewModel()
@IBOutlet weak var fibkles: SpeakEleLoading!
override func viewDidLoad() {
super.viewDidLoad()
SpeakElePublicManager.share.PublicData.state = .qanda
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
titleView.isHidden = true
fibkles.startAnimating()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
SpeakContentV.cornerRect(radius: 16, [.topLeft,.topRight])
}
override func PopViewCtr() {
let navigations = self.navigationController?.viewControllers
var nav = navigations?.filter({ !($0 is SpeakEleThrDaysFreeViewCtr) })
nav?.removeLast()
if nav?.count ?? 0 > 0 {
self.navigationController?.viewControllers = nav ?? []
}else{
self.navigationController?.popViewController(animated: true)
}
}
@IBAction func SpeakSendMsg(_ sender: Any) {
}
}
......@@ -11,6 +11,11 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SpeakEleQAViewCtr" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<connections>
<outlet property="SpeakContentV" destination="lhO-rO-ci0" id="VH2-3v-YWq"/>
<outlet property="SpeakInputBox" destination="n0s-ld-qq8" id="UMQ-9p-FAo"/>
<outlet property="SpeakProgres" destination="ZV3-Si-Hef" id="ZBs-da-fiI"/>
<outlet property="SpeakTableView" destination="cg4-Ok-TTr" id="vHr-lh-zK2"/>
<outlet property="fibkles" destination="21f-vv-8my" id="ksF-xm-4gz"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
......@@ -18,14 +23,120 @@
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lwe-eP-frU">
<rect key="frame" x="0.0" y="0.0" width="393" height="300"/>
<color key="backgroundColor" systemColor="systemGrayColor"/>
<constraints>
<constraint firstAttribute="height" constant="300" id="nZN-iZ-9Sw"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lhO-rO-ci0">
<rect key="frame" x="0.0" y="270" width="393" height="514"/>
<subviews>
<slider opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="ZV3-Si-Hef">
<rect key="frame" x="22" y="27" width="349" height="31"/>
<color key="minimumTrackTintColor" red="0.41960784313725491" green="0.44313725490196076" blue="0.8901960784313725" alpha="1" colorSpace="calibratedRGB"/>
<color key="thumbTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</slider>
<tableView clipsSubviews="YES" contentMode="scaleToFill" ambiguous="YES" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="cg4-Ok-TTr">
<rect key="frame" x="0.0" y="106" width="393" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</tableView>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ly6-as-jqg">
<rect key="frame" x="0.0" y="429" width="393" height="80"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Pk3-rK-Odt">
<rect key="frame" x="0.0" y="0.0" width="393" height="0.66666666666666663"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.12643494897959182" colorSpace="custom" customColorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="0.59999999999999998" id="Yvd-K3-8wM"/>
</constraints>
</view>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="n0s-ld-qq8" customClass="SpeakResizableTextView" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<rect key="frame" x="24" y="16" width="281" height="48"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="Enter what you want to say"/>
<userDefinedRuntimeAttribute type="number" keyPath="maxLines">
<integer key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="placeholderColor">
<color key="value" red="0.0" green="0.0" blue="0.0" alpha="0.26086840986394561" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="da4-jx-50Y">
<rect key="frame" x="321" y="16.666666666666629" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" secondItem="da4-jx-50Y" secondAttribute="height" multiplier="1:1" id="27f-oZ-xih"/>
<constraint firstAttribute="width" constant="48" id="8Bh-es-kmZ"/>
</constraints>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="sp-guide-Send"/>
<connections>
<action selector="SpeakSendMsg:" destination="-1" eventType="touchUpInside" id="pSi-TS-fMS"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="da4-jx-50Y" firstAttribute="top" secondItem="Pk3-rK-Odt" secondAttribute="bottom" constant="16" id="4dg-nq-tuG"/>
<constraint firstItem="n0s-ld-qq8" firstAttribute="top" secondItem="ly6-as-jqg" secondAttribute="top" constant="16" id="NDK-Dg-doG"/>
<constraint firstItem="Pk3-rK-Odt" firstAttribute="top" secondItem="ly6-as-jqg" secondAttribute="top" id="R6c-cV-kgx"/>
<constraint firstItem="n0s-ld-qq8" firstAttribute="leading" secondItem="ly6-as-jqg" secondAttribute="leading" constant="24" id="WtK-gG-9YY"/>
<constraint firstAttribute="bottom" secondItem="n0s-ld-qq8" secondAttribute="bottom" constant="16" id="ZzS-4N-1Ys"/>
<constraint firstItem="da4-jx-50Y" firstAttribute="leading" secondItem="n0s-ld-qq8" secondAttribute="trailing" constant="16" id="b8d-Nb-tRd"/>
<constraint firstItem="Pk3-rK-Odt" firstAttribute="leading" secondItem="ly6-as-jqg" secondAttribute="leading" id="jeu-9r-eGu"/>
<constraint firstAttribute="trailing" secondItem="Pk3-rK-Odt" secondAttribute="trailing" id="oWt-sh-GoR"/>
<constraint firstAttribute="trailing" secondItem="da4-jx-50Y" secondAttribute="trailing" constant="24" id="zEc-CZ-CLq"/>
</constraints>
</view>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="21f-vv-8my" customClass="SpeakEleLoading" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<rect key="frame" x="54" y="206" width="240" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ZV3-Si-Hef" secondAttribute="trailing" constant="24" id="1nI-Z7-Y5k"/>
<constraint firstItem="ZV3-Si-Hef" firstAttribute="leading" secondItem="lhO-rO-ci0" secondAttribute="leading" constant="24" id="AQl-pO-YQU"/>
<constraint firstItem="cg4-Ok-TTr" firstAttribute="leading" secondItem="lhO-rO-ci0" secondAttribute="leading" id="GjK-oI-Kbf"/>
<constraint firstAttribute="trailing" secondItem="cg4-Ok-TTr" secondAttribute="trailing" id="SGl-3z-sKS"/>
<constraint firstItem="ly6-as-jqg" firstAttribute="leading" secondItem="lhO-rO-ci0" secondAttribute="leading" id="SpG-iO-USd"/>
<constraint firstAttribute="trailing" secondItem="ly6-as-jqg" secondAttribute="trailing" id="bL4-Cd-dnY"/>
<constraint firstItem="cg4-Ok-TTr" firstAttribute="top" secondItem="ZV3-Si-Hef" secondAttribute="bottom" constant="27" id="h9d-fA-6I2"/>
<constraint firstItem="ly6-as-jqg" firstAttribute="top" secondItem="cg4-Ok-TTr" secondAttribute="bottom" id="oww-tc-Bmb"/>
<constraint firstItem="ZV3-Si-Hef" firstAttribute="top" secondItem="lhO-rO-ci0" secondAttribute="top" constant="27" id="vBc-sU-B4D"/>
<constraint firstAttribute="bottom" secondItem="ly6-as-jqg" secondAttribute="bottom" constant="5" id="x6h-Uj-KTG"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<color key="backgroundColor" systemColor="systemYellowColor"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="lhO-rO-ci0" secondAttribute="trailing" id="0pg-BK-4Xd"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="lwe-eP-frU" secondAttribute="trailing" id="3I4-fE-ESg"/>
<constraint firstItem="lhO-rO-ci0" firstAttribute="top" secondItem="lwe-eP-frU" secondAttribute="bottom" constant="-30" id="H2X-UV-LNn"/>
<constraint firstItem="lwe-eP-frU" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="YP7-Ux-iGd"/>
<constraint firstItem="lwe-eP-frU" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="eg0-KR-rSI"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="lhO-rO-ci0" secondAttribute="bottom" id="mPb-3N-74R"/>
<constraint firstItem="lhO-rO-ci0" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="nlJ-qr-kRp"/>
</constraints>
<point key="canvasLocation" x="131" y="-11"/>
</view>
</objects>
<resources>
<systemColor name="systemYellowColor">
<color red="1" green="0.80000000000000004" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<image name="sp-guide-Send" width="48" height="48"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGrayColor">
<color red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
......@@ -17,15 +17,9 @@ class SpeakEleThrDaysFreeViewCtr: SpeakEleBaseViewCtr {
}
private func toIAP() -> Void {
SpeakElePublicManager.share.PublicData.state = .subscribe
let iap = SpeakEleIAPViewCtr()
iap.modalPresentationStyle = .overFullScreen
self.present(iap, animated: true)
iap.callback = { [weak self] in
let QAndA = SpeakEleQAViewCtr()
var array = self?.navigationController?.viewControllers
array?.removeLast()
array?.append(QAndA)
self?.navigationController?.viewControllers = array ?? [QAndA]
}
iap.navigationController?.AnimationState = .present
self.navigationController?.pushViewController(iap, animated: true)
}
}
//
// SpeakEleQAModel.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
enum SpeakRoleState : Int,Codable{
case speakAI = 0
case speakUser = 1
}
enum SpeakMessageType:Int,Codable {
case none = 0 //
case name = 1 // 姓名
case language = 2 // 母语
case like = 3 // 偏好的语言
case hobby = 4 // 爱好
case improving = 5 // 改善
case proficiency = 6// 目前水平
case goalDays = 7 // 目标时间
case studytime = 8 // 学习时间
case practice = 9 // 练习时间
case finish = 10 // 结束
}
struct SpeakEleQAModel : Codable {
var speakMsg:String = ""
var role:SpeakRoleState = .speakAI
var messageType:SpeakMessageType = .none
}
//
// SpeakEleQAViewModel.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakEleQAViewModel: NSObject {
var questionArrys:[SpeakEleQAModel] {
get {
return [
SpeakEleQAModel(speakMsg: "Hello! I’m Speakly, an AI tutor designed just for you. I’m here to assist with boosting your English skills." ),
SpeakEleQAModel(speakMsg: "May I know your name?" ,messageType: .name),
SpeakEleQAModel(speakMsg: "Nice to make your acquaintance, $$$ !"),
SpeakEleQAModel(speakMsg: "I have a few questions to help me get a better sense of you."),
SpeakEleQAModel(speakMsg: "What language do you speak natively?",messageType: .language),
SpeakEleQAModel(speakMsg: "Would you prefer English lessons with explanations in $$$ or in English itself?",messageType: .like),
SpeakEleQAModel(speakMsg: "Go ahead and pick your interests. You can select a maximum of 4 choices." , messageType: .hobby),
SpeakEleQAModel(speakMsg: "Which aspects of English do you want to work on improving?",messageType: .improving),
SpeakEleQAModel(speakMsg: "How would you describe your current English proficiency?",messageType: .proficiency),
SpeakEleQAModel(speakMsg: "How soon do you hope to achieve your English learning goals?",messageType: .goalDays),
SpeakEleQAModel(speakMsg: "What’s your daily target for practice?",messageType: .studytime),
SpeakEleQAModel(speakMsg: "Choose a specific time each day when you’d like to start your practice sessions.",messageType: .practice),
SpeakEleQAModel(speakMsg: "I’ll send you a reminder for your practice time every day. To ensure you don’t skip a session, please enable notifications!", messageType: .finish)
]
}
}
}
......@@ -9,17 +9,19 @@ import UIKit
class SpeakEleIAPViewCtr: UIViewController {
var callback:(()->Void)?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func SpeakCloseTaps(_ sender: Any) {
self.dismiss(animated: true) {
guard let call = self.callback else { return }
call()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.AnimationState = .none
}
@IBAction func SpeakCloseTaps(_ sender: Any) {
let QAndA = SpeakEleQAViewCtr()
self.navigationController?.pushViewController(QAndA, animated: true)
}
}
......@@ -12,13 +12,31 @@ class SpeakEleLaunchViewCtr: SpeakEleBaseViewCtr {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.toLogin()
self.ToPages()
}
}
private func toLogin() -> Void {
private func ToPages() -> Void {
switch SpeakElePublicManager.share.PublicData.state {
case .start:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleLoginGuideCtr())
window.switchToController(nav)
break
case .subscribe:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleIAPViewCtr())
window.switchToController(nav)
case .qanda:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleQAViewCtr())
window.switchToController(nav)
case .login:
break
default:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleTabbarViewCtr())
window.switchToController(nav)
break
}
}
}
......@@ -12,19 +12,27 @@ import YYText
class SpeakEleLoginGuideCtr: SpeakEleBaseViewCtr {
@IBOutlet weak var SpeakDescLabel: UILabel!
@IBOutlet weak var speakLetsGo: SpeakEleButton!
@IBOutlet weak var speakLoginBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
SpeakDescLabel.setCutomFonts()
speakLetsGo.setCutomFonts()
speakLoginBtn.setCutomFonts()
setDescription()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
@IBAction func SpeakLogin(_ sender: Any) {
let login = SpeakEleCreatAViewCtr()
let login = SpeakEleLoginViewCtr()
self.navigationController?.pushViewController(login, animated: true)
}
......
......@@ -12,6 +12,8 @@
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SpeakEleLoginGuideCtr" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<connections>
<outlet property="SpeakDescLabel" destination="jGO-Du-a3k" id="mxV-nA-Nwk"/>
<outlet property="speakLetsGo" destination="Zwf-M5-Hrt" id="GqW-aT-1s1"/>
<outlet property="speakLoginBtn" destination="GOS-gl-Ilh" id="951-M8-75o"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
......
//
// SpeakEleLoginViewCtr.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakEleLoginViewCtr: SpeakEleBaseViewCtr {
override func viewDidLoad() {
super.viewDidLoad()
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SpeakEleLoginViewCtr" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<connections>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="eoM-SK-uFc">
<rect key="frame" x="24" y="168" width="345" height="95"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome back" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ngd-gr-rPf">
<rect key="frame" x="0.0" y="0.0" width="345" height="38.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="0.89859693877551017" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cutomFont">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="fontSize">
<real key="value" value="32"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Log in to continue learning with your private AI Tutor" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Wve-dN-jIa">
<rect key="frame" x="0.0" y="54.333333333333343" width="345" height="40.666666666666657"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="0.53768069727891155" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cutomFont">
<integer key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="fontSize">
<real key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="3vF-f8-EXC">
<rect key="frame" x="24" y="287" width="345" height="186"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xbG-3d-KMi">
<rect key="frame" x="0.0" y="0.0" width="345" height="60"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="E-mail address" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="M4R-vQ-irk">
<rect key="frame" x="16" y="19" width="313" height="22"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits"/>
</textField>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.041746386054421769" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="60" id="4d3-yx-5wP"/>
<constraint firstAttribute="trailing" secondItem="M4R-vQ-irk" secondAttribute="trailing" constant="16" id="EmW-oh-uos"/>
<constraint firstItem="M4R-vQ-irk" firstAttribute="leading" secondItem="xbG-3d-KMi" secondAttribute="leading" constant="16" id="JJb-x0-8Lg"/>
<constraint firstAttribute="bottom" secondItem="M4R-vQ-irk" secondAttribute="bottom" constant="19" id="bwe-Tm-79f"/>
<constraint firstItem="M4R-vQ-irk" firstAttribute="top" secondItem="xbG-3d-KMi" secondAttribute="top" constant="19" id="dRM-xb-RT8"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="corners">
<real key="value" value="8"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="khn-vx-1Kx">
<rect key="frame" x="0.0" y="76" width="345" height="60"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="RcT-ZC-XJJ">
<rect key="frame" x="16" y="19" width="263" height="22"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8b2-4s-WXV">
<rect key="frame" x="295" y="13" width="34" height="34"/>
<inset key="contentEdgeInsets" minX="5" minY="5" maxX="5" maxY="5"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="sp-login-ps-hide"/>
<state key="selected" image="sp-login-ps-show"/>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.041746386050000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="RcT-ZC-XJJ" secondAttribute="bottom" constant="19" id="3hl-jG-xAR"/>
<constraint firstItem="8b2-4s-WXV" firstAttribute="centerY" secondItem="khn-vx-1Kx" secondAttribute="centerY" id="5r3-0K-9WL"/>
<constraint firstItem="RcT-ZC-XJJ" firstAttribute="top" secondItem="khn-vx-1Kx" secondAttribute="top" constant="19" id="ChJ-mq-2dI"/>
<constraint firstItem="RcT-ZC-XJJ" firstAttribute="leading" secondItem="khn-vx-1Kx" secondAttribute="leading" constant="16" id="MNT-l9-oeP"/>
<constraint firstAttribute="trailing" secondItem="8b2-4s-WXV" secondAttribute="trailing" constant="16" id="ebv-KX-Wal"/>
<constraint firstItem="8b2-4s-WXV" firstAttribute="leading" secondItem="RcT-ZC-XJJ" secondAttribute="trailing" constant="16" id="q3H-1g-kxT"/>
<constraint firstAttribute="height" constant="60" id="zoi-Dq-eSE"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="corners">
<real key="value" value="8"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LHb-rx-f3q">
<rect key="frame" x="0.0" y="152" width="345" height="34"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Forgot password?">
<color key="titleColor" red="0.0" green="0.0" blue="0.0" alpha="0.5385044642857143" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cutomFont">
<integer key="value" value="0"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="fontSize">
<real key="value" value="16"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="eoM-SK-uFc" secondAttribute="trailing" constant="24" id="3jB-Uf-0Ha"/>
<constraint firstItem="eoM-SK-uFc" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="50" id="HVz-5P-XPj"/>
<constraint firstItem="3vF-f8-EXC" firstAttribute="top" secondItem="eoM-SK-uFc" secondAttribute="bottom" constant="24" id="JqS-Vo-WOy"/>
<constraint firstItem="3vF-f8-EXC" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="24" id="bex-a9-cBw"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="3vF-f8-EXC" secondAttribute="trailing" constant="24" id="jtB-es-zHL"/>
<constraint firstItem="eoM-SK-uFc" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="24" id="vMQ-Qy-HVM"/>
</constraints>
<point key="canvasLocation" x="131" y="-12"/>
</view>
</objects>
<resources>
<image name="sp-login-ps-hide" width="24" height="24"/>
<image name="sp-login-ps-show" width="24" height="24"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
//
// SpeakEleAnimation.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakCustomPushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.35
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
toVC.view.frame = CGRect(x: 0, y: containerView.bounds.height,
width: containerView.bounds.width, height: containerView.bounds.height)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.5,
options: .curveEaseInOut) {
toVC.view.frame = containerView.bounds
fromVC.view.frame = CGRect(x: 0, y: 0,
width: containerView.bounds.width, height: containerView.bounds.height)
} completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
//
// SpeakEleLoading.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
@IBDesignable
class SpeakEleLoading : UIView {
// MARK: - 公开属性
/// 圆点颜色
@IBInspectable var dotColor: UIColor = .gray {
didSet { dots.forEach { $0.backgroundColor = dotColor } }
}
/// 圆点大小
@IBInspectable var dotSize: CGFloat = 8 {
didSet { updateDotFrames() }
}
/// 圆点间距
@IBInspectable var spacing: CGFloat = 8 {
didSet { updateDotFrames() }
}
/// 动画持续时间(秒)
@IBInspectable var animationDuration: TimeInterval = 0.4
var contentInsets:UIEdgeInsets = UIEdgeInsets()
// MARK: - 私有属性
private var dots: [UIView] = []
private var isAnimating = false
// MARK: - 初始化
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - 视图设置
private func setup() {
NotificationCenter.default.addObserver(
self,
selector: #selector(didBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(willEnterForeground(_:)),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
// 创建三个圆点
for _ in 0..<3 {
let dot = UIView()
dot.backgroundColor = dotColor
dot.layer.cornerRadius = dotSize / 2
dot.alpha = 0.3
addSubview(dot)
dots.append(dot)
}
updateDotFrames()
}
private func updateDotFrames() {
guard dots.count == 3 else { return }
let totalWidth = dotSize * 3 + spacing * 2
let startX = (bounds.width - totalWidth) / 2
for (index, dot) in dots.enumerated() {
let x = startX + CGFloat(index) * (dotSize + spacing)
dot.frame = CGRect(x: x, y: (bounds.height - dotSize) / 2, width: dotSize, height: dotSize)
}
}
// MARK: - 动画控制
func startAnimating() {
guard !isAnimating else { return }
isAnimating = true
for (index, dot) in dots.enumerated() {
let delay = Double(index) * animationDuration / Double(dots.count)
animateDot(dot, withDelay: delay)
}
}
func stopAnimating() {
isAnimating = false
dots.forEach { $0.layer.removeAllAnimations() }
dots.forEach { $0.alpha = 0.3 }
}
private func animateDot(_ dot: UIView, withDelay delay: TimeInterval) {
// 创建透明度动画
let pulseAnimation = CABasicAnimation(keyPath: "opacity")
pulseAnimation.fromValue = 0.3
pulseAnimation.toValue = 1.0
pulseAnimation.duration = animationDuration
pulseAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
pulseAnimation.repeatCount = .infinity
pulseAnimation.autoreverses = true
pulseAnimation.beginTime = CACurrentMediaTime() + delay
dot.layer.add(pulseAnimation, forKey: "pulse")
}
// MARK: - 布局
override func layoutSubviews() {
super.layoutSubviews()
updateDotFrames()
}
@objc private func didBecomeActive(_ notication:Notification) -> Void {
self.startAnimating()
}
@objc private func willEnterForeground(_ notication:Notification) -> Void {
self.stopAnimating()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
//
// SpeakKeyboardManager.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakKeyboardManager: NSObject {
static let share = SpeakKeyboardManager()
func monitor() -> Void {
// 监听键盘事件
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
// MARK: - 键盘处理
@objc private func keyboardWillShow(_ notification: Notification) {
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ,
let view = notification.object as? UIView else { return }
// let keyboardTop = window.convert(keyboardFrame, from: nil).minY
//
// let textViewBottom = window.convert(bounds, from: superview).maxY
// let overlap = textViewBottom - keyboardTop + 10 // 额外间距
let overlap = keyboardFrame.height - window.safeAreaInsets.bottom
if overlap > 0 {
var frame = window.frame
frame.origin.y -= overlap
window.frame = frame
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
window.frame = UIScreen.main.bounds
}
private override init() {
}
}
extension UIWindow {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let frame = self.frame
if frame.origin.y < 0 {
self.endEditing(false)
return nil
}else{
return super.hitTest(point, with: event)
}
}
}
//
// SpeakSendMsgBox.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakResizableTextView: UIView {
// MARK: - 公开属性
/// 文本内容
@IBInspectable var text: String? {
get { return textView.text }
set { textView.text = newValue }
}
/// 占位符文本
@IBInspectable var placeholder: String? {
get { return placeholderLabel.text }
set { placeholderLabel.text = newValue }
}
/// 占位符颜色
@IBInspectable var placeholderColor: UIColor = .lightGray {
didSet { placeholderLabel.textColor = placeholderColor }
}
/// 文本颜色
@IBInspectable var textColor: UIColor? {
get { return textView.textColor }
set { textView.textColor = newValue }
}
/// 字体
var font: UIFont? {
get { return textView.font }
set { textView.font = newValue }
}
/// 最大行数限制(0 表示无限制)
@IBInspectable var maxLines: Int = 0 {
didSet { updateMaxHeight() }
}
/// 文本变化回调
var onTextChanged: ((String) -> Void)?
/// 达到最大行数回调
var onReachMaxLines: (() -> Void)?
// MARK: - 私有属性
private let textView = UITextView()
private let placeholderLabel = UILabel()
private var maxHeight: CGFloat = .greatestFiniteMagnitude
private var originalInset: UIEdgeInsets?
// MARK: - 初始化
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
setupConstraints()
}
// MARK: - 视图设置
private func setupViews() {
// 设置背景和边框
backgroundColor = .white
layer.cornerRadius = 8
layer.borderWidth = 1
layer.borderColor = UIColor.lightGray.cgColor
// 配置 UITextView
textView.delegate = self
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 16)
textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
textView.keyboardDismissMode = .onDrag
addSubview(textView)
// 配置占位符标签
placeholderLabel.textColor = placeholderColor
placeholderLabel.font = textView.font
placeholderLabel.numberOfLines = 0
placeholderLabel.isUserInteractionEnabled = false
addSubview(placeholderLabel)
}
private func setupConstraints() {
textView.translatesAutoresizingMaskIntoConstraints = false
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
textView.snp.makeConstraints { make in
make.left.top.bottom.right.equalToSuperview().inset(UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4))
make.height.greaterThanOrEqualTo(30)
}
placeholderLabel.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.left.equalToSuperview().inset(16)
}
}
// MARK: - 高度计算和更新
private func updateHeight() {
let size = CGSize(width: textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right,
height: .greatestFiniteMagnitude)
let estimatedSize = textView.sizeThatFits(size)
let newHeight = min(estimatedSize.height, maxHeight)
var frame = self.frame
frame.size.height = newHeight
self.frame = frame
placeholderLabel.isHidden = !textView.text.isEmpty
onTextChanged?(textView.text)
}
private func updateMaxHeight() {
if maxLines > 0, let font = textView.font {
let lineHeight = font.lineHeight
maxHeight = lineHeight * CGFloat(maxLines) + textView.textContainerInset.top + textView.textContainerInset.bottom
} else {
maxHeight = .greatestFiniteMagnitude
}
}
private func findScrollView() -> UIScrollView? {
var view: UIView? = self
while let superview = view?.superview {
if let scrollView = superview as? UIScrollView {
if originalInset == nil {
originalInset = scrollView.contentInset
}
return scrollView
}
view = superview
}
return nil
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// MARK: - UITextViewDelegate
extension SpeakResizableTextView : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updateHeight()
if maxLines > 0 {
let size = CGSize(width: textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right,
height: .greatestFiniteMagnitude)
let estimatedSize = textView.sizeThatFits(size)
if estimatedSize.height >= maxHeight {
onReachMaxLines?()
}
}
}
func textViewDidBeginEditing(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
}
func textViewDidEndEditing(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment